Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
<a href="README.zh-CN.md"><strong>简体中文</strong></a>
</p>

[Download MaxMath 1.0.1 APK](https://github.com/yueye6811/MaxMath/releases/download/v1.0.1/MaxMath-v1.0.1-arm64-v8a.apk)
· [Release notes](https://github.com/yueye6811/MaxMath/releases/tag/v1.0.1)
[Download MaxMath 1.1.0 APK](https://github.com/yueye6811/MaxMath/releases/download/v1.1.0/MaxMath-v1.1.0-arm64-v8a.apk)
· [Release notes](https://github.com/yueye6811/MaxMath/releases/tag/v1.1.0)

MaxMath is an offline Android app for higher-algebra computation and interactive
plotting, powered by GNU Maxima. Its interface is built with Kotlin and Jetpack
Compose, mathematical input is handled by a pure Kotlin parser, and complex
symbolic computations run in a separate engine process.

> Current release: 1.0.1. Minimum supported version: Android 8.0 (API 26).
> Current release: 1.1.0. Minimum supported version: Android 8.0 (API 26).
> The native computation engine currently provides an `arm64-v8a` build
> workflow only.

Expand All @@ -33,6 +33,7 @@ symbolic computations run in a separate engine process.
- Input: implicit multiplication, radicals, fractions, common functions, π/e, and natural equation syntax
- Output: offline LaTeX rendering, copyable text, and PNG saving or sharing
- Localization: Chinese, English, or the system language
- Appearance: complete light and dark themes, including interactive plots
- Privacy: no network permission, accounts, ads, analytics, or telemetry

## Architecture
Expand Down
7 changes: 4 additions & 3 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
<strong>简体中文</strong>
</p>

[下载 MaxMath 1.0.1 APK](https://github.com/yueye6811/MaxMath/releases/download/v1.0.1/MaxMath-v1.0.1-arm64-v8a.apk)
· [发布说明](https://github.com/yueye6811/MaxMath/releases/tag/v1.0.1)
[下载 MaxMath 1.1.0 APK](https://github.com/yueye6811/MaxMath/releases/download/v1.1.0/MaxMath-v1.1.0-arm64-v8a.apk)
· [发布说明](https://github.com/yueye6811/MaxMath/releases/tag/v1.1.0)

基于 GNU Maxima 的离线 Android 高等代数计算与交互式绘图应用。界面使用
Kotlin 与 Jetpack Compose,数学输入由纯 Kotlin 解析器处理,复杂符号计算在独立
引擎进程中执行。

> 当前版本 1.0.1;最低系统 Android 8.0(API 26);原生计算引擎目前仅提供
> 当前版本 1.1.0;最低系统 Android 8.0(API 26);原生计算引擎目前仅提供
> `arm64-v8a` 构建流程。

## 功能
Expand All @@ -31,6 +31,7 @@ Kotlin 与 Jetpack Compose,数学输入由纯 Kotlin 解析器处理,复杂
- 输入:隐式乘法、根号、分数、常用函数、π/e 与自然写法方程组
- 输出:离线 LaTeX 渲染,可复制文本,并可保存或分享 PNG
- 本地化:中文、English、跟随系统
- 外观:完整浅色/深色主题,交互式绘图同步适配
- 隐私:不申请网络权限,不包含账号、广告、分析或遥测

## 架构
Expand Down
19 changes: 19 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
# MaxMath 1.1.0

界面、深色模式与交互体验升级版(2026-08-08)。

## 本版改进

- 建立完整的 Material 3 浅色/深色主题,统一配色、字体、圆角、间距和尺寸;
2D/3D/等高线绘图同步适配主题,深色模式冷启动不再白闪。
- 所有计算页面加入运行中取消入口;重算时保留上一结果,取消后恢复原状态,
新结果完成后自动滚动到结果区。
- 重新设计加载与错误卡片:首次启动引擎时给出提示,错误详情可展开、复制,
Maxima 纯文本结果使用等宽排版。
- 改进数学输入键盘行为,关闭自动纠错和自动大写;极限与积分的符号边界使用
文本键盘;矩阵、向量和二次型的单元格内容可在屏幕旋转后恢复。
- 加入带方向感的页面切换动画、统一模块卡片和无障碍标签;绘图重算期间保留并
淡化上一预览,导出的 PNG 仍保持适合文档分享的浅色背景。
- 新增计算取消、状态恢复、矩阵保存器、绘图配色和 WCAG AA 对比度测试;
release 变体统一排除依赖 Compose 测试清单的 `*StateTest`。

# MaxMath 1.0.1

性能与体积优化版(2026-08-07),详见 `docs/OPTIMIZATION.md`。
Expand Down
9 changes: 6 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ android {
applicationId = "com.paruh.maxmath"
minSdk = 26
targetSdk = 36
versionCode = 15
versionName = "1.0.1"
versionCode = 16
versionName = "1.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down Expand Up @@ -124,6 +124,9 @@ tasks.withType<Test>().configureEach {
// Compose UI 测试依赖 debugImplementation 的 ui-test-manifest(其中声明了
// exported 测试 Activity),release 变体不应打包该测试组件;
// UI 行为与变体无关,debug 变体已覆盖。
exclude("**/*ScreenStateTest.class")
//
// 约定:所有用 createComposeRule 的测试都以 StateTest 结尾。原先只排除
// *ScreenStateTest,组件级的 *SectionStateTest 会漏网并在 release 变体炸掉。
exclude("**/*StateTest.class")
}
}
21 changes: 18 additions & 3 deletions app/src/main/java/com/paruh/maxmath/ui/CalcViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,18 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.UUID

/**
* @property loading 是否有计算在跑。
* @property response 最近一次成功的结果;重算期间**不清空**,界面因此不会塌陷。
* @property error 最近一次失败的原因;发起新计算时清掉。
*
* 「结果已过期」不单独存字段——它就是 `loading && response != null`,
* 再加一个标志位只会多一份可能对不上的真相。
*/
data class CalcUiState(
val loading: Boolean = false,
val response: CalcResponse? = null,
Expand All @@ -42,10 +51,15 @@ class CalcViewModel : AndroidViewModel {
fun compute(task: MathTask) {
job?.cancel()
MaximaEngine.cancel()
_state.value = CalcUiState(loading = true)
// 保留上一次结果:重算时不清空,界面不会塌陷再展开。
// 错误必须清掉,否则旧报错会和新的进度指示并排显示。
_state.update { it.copy(loading = true, error = null) }
job = viewModelScope.launch {
val request = CalcRequest(UUID.randomUUID().toString(), task)
val response = client.compute(request)
// 这里整体赋值:一次计算结束后,旧结果就该被新结果或新错误取代。
// 若协程已被 cancel(),withContext 恢复时会先抛 CancellationException,
// 走不到这一行,因此被取消的计算不会覆盖主线程刚写好的状态。
_state.value = CalcUiState(
loading = false,
response = response.takeIf { it.ok },
Expand All @@ -54,14 +68,15 @@ class CalcViewModel : AndroidViewModel {
}
}

/** 取消的是这次请求,不是整个界面:结果停留在取消前的样子。 */
fun cancel() {
job?.cancel()
MaximaEngine.cancel()
_state.value = CalcUiState()
_state.update { it.copy(loading = false) }
}

fun showError(message: String?) {
val text = message ?: AppLocale.wrap(getApplication()).getString(R.string.error_input_invalid)
_state.value = CalcUiState(error = text)
_state.update { it.copy(loading = false, error = text) }
}
}
36 changes: 36 additions & 0 deletions app/src/main/java/com/paruh/maxmath/ui/MatrixCells.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.paruh.maxmath.ui

import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver

/** 不可变更新矩阵中的单个单元格,供矩阵类表单共享。 */
fun updateCell(
cells: List<List<String>>,
Expand All @@ -14,3 +17,36 @@ fun updateCell(
r
}
}

/**
* `List<List<String>>` 的 Saver,用于矩阵/向量/二次型三个表单的单元格。
*
* 这三处原先用的是普通 `remember`,旋转屏幕后行列数还在、格子里填的东西全没了。
* 换成 `rememberSaveable` 时不能依赖默认路径:默认路径能不能work取决于
* `List(n){}` 在运行时恰好返回 `ArrayList`(Serializable),换个 List 实现就会在
* 旋转时崩,而单元测试根本发现不了。所以显式声明。
*
* 编码成扁平列表 `[行数, 列数, v00, v01, …, v10, …]`——Int 和 String 都是
* Bundle 原生支持的类型。刻意不做分隔符拼接:单元格内容可以是任意表达式,
* 里面出现逗号或分号会直接把数据切坏。
*
* 三个调用点的行长都一致(都用 `List(n) { List(m) { … } }` 构造),所以只带
* 行列数就够;restore 里的 `getOrNull`/`as?` 是防御性的,正常路径走不到。
*/
val MatrixCellsSaver: Saver<List<List<String>>, Any> = listSaver(
save = { cells ->
buildList {
add(cells.size)
add(cells.firstOrNull()?.size ?: 0)
cells.forEach { row -> addAll(row) }
}
},
restore = { flat ->
val rows = flat.getOrNull(0) as? Int ?: 0
val cols = flat.getOrNull(1) as? Int ?: 0
val values = flat.drop(2)
List(rows) { r ->
List(cols) { c -> values.getOrNull(r * cols + c) as? String ?: "" }
}
},
)
57 changes: 48 additions & 9 deletions app/src/main/java/com/paruh/maxmath/ui/MaxMathApp.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.paruh.maxmath.ui

import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
Expand All @@ -13,32 +17,67 @@ import com.paruh.maxmath.ui.screens.QuadraticFormScreen
import com.paruh.maxmath.ui.screens.SystemScreen
import com.paruh.maxmath.ui.screens.VectorSpaceScreen

/** 路由常量:首页的模块表和这里的 NavHost 都引用它,避免两处字符串写岔。 */
object Routes {
const val HOME = "home"
const val MATRIX = "matrix"
const val SYSTEM = "system"
const val POLYNOMIAL = "polynomial"
const val VECTOR = "vector"
const val QUADRATIC = "quadratic"
const val CALCULUS = "calculus"
const val PLOT = "plot"
}

private const val NAV_DURATION_MS = 280

@Composable
fun MaxMathApp() {
val nav = rememberNavController()
NavHost(navController = nav, startDestination = "home") {
composable("home") {
NavHost(
navController = nav,
startDestination = Routes.HOME,
// 前进向左推、返回向右推:给「首页 → 模块」这层关系一个方向感,
// 原先是默认淡入淡出,看不出层级。
enterTransition = {
slideIntoContainer(SlideDirection.Start, tween(NAV_DURATION_MS)) +
fadeIn(tween(NAV_DURATION_MS))
},
exitTransition = {
slideOutOfContainer(SlideDirection.Start, tween(NAV_DURATION_MS)) +
fadeOut(tween(NAV_DURATION_MS))
},
popEnterTransition = {
slideIntoContainer(SlideDirection.End, tween(NAV_DURATION_MS)) +
fadeIn(tween(NAV_DURATION_MS))
},
popExitTransition = {
slideOutOfContainer(SlideDirection.End, tween(NAV_DURATION_MS)) +
fadeOut(tween(NAV_DURATION_MS))
},
) {
composable(Routes.HOME) {
HomeScreen(onNavigate = { route -> nav.navigate(route) })
}
composable("matrix") {
composable(Routes.MATRIX) {
MatrixScreen(onBack = { nav.popBackStack() })
}
composable("system") {
composable(Routes.SYSTEM) {
SystemScreen(onBack = { nav.popBackStack() })
}
composable("polynomial") {
composable(Routes.POLYNOMIAL) {
PolynomialScreen(onBack = { nav.popBackStack() })
}
composable("vector") {
composable(Routes.VECTOR) {
VectorSpaceScreen(onBack = { nav.popBackStack() })
}
composable("quadratic") {
composable(Routes.QUADRATIC) {
QuadraticFormScreen(onBack = { nav.popBackStack() })
}
composable("calculus") {
composable(Routes.CALCULUS) {
CalculusScreen(onBack = { nav.popBackStack() })
}
composable("plot") {
composable(Routes.PLOT) {
PlotScreen(onBack = { nav.popBackStack() })
}
}
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/paruh/maxmath/ui/ModeIcons.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ object ModeIcons {
),
)

/**
* 步进器的减号。material-icons-core 只有 Add 没有 Remove(Remove 在
* material-icons-extended 里,那个包会给 APK 加几 MB),所以自绘一条。
* 尺寸刻意对齐 Material 的 Add 字形:横向 5→19,厚度 11→13。
*
* **不要放进 [all]** —— ModeIconsTest 断言 all.size == 7,那是首页模块图标集,
* 减号不属于它。
*/
val Minus: ImageVector = icon("Minus", paths = listOf("M5,11 H19 V13 H5 Z"))

val all: List<ImageVector> = listOf(Matrix, Equations, Polynomial, Vector, Quadratic, Calculus, Plot)

private fun icon(
Expand Down
Loading
Loading