From d0b133a8955e24a06236a74ccb2a4509739d1ca8 Mon Sep 17 00:00:00 2001 From: ZXCLI Date: Mon, 7 Sep 2026 13:17:19 +0800 Subject: [PATCH] android: lock portrait - UI composition glued to phone frame (top/bottom bars + image area absolutely fixed) --- analysis/preview/preview_portrait_v5.png | 3 +++ analysis/preview/preview_rot0_v5.png | 3 +++ analysis/preview/preview_rot180_v5.png | 3 +++ analysis/preview/preview_rot270_v5.png | 3 +++ analysis/preview/preview_rot90_v5.png | 3 +++ android/app/src/main/AndroidManifest.xml | 2 +- .../kotlin/com/mag160c/thermal/ui/AppRoot.kt | 9 +++++---- .../kotlin/com/mag160c/thermal/ui/UiInsets.kt | 2 +- .../thermal/ui/gallery/GalleryScreen.kt | 7 +------ .../mag160c/thermal/ui/live/LiveRenderer.kt | 12 ++++++----- .../com/mag160c/thermal/ui/live/LiveScreen.kt | 12 ++++++----- build-artifacts/mag160c-app-debug.apk | 2 +- docs/android_app/HANDOFF_DEVELOPMENT.md | 20 +++++++++++-------- docs/android_app/session_state.md | 17 ++++++++++++++++ 14 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 analysis/preview/preview_portrait_v5.png create mode 100644 analysis/preview/preview_rot0_v5.png create mode 100644 analysis/preview/preview_rot180_v5.png create mode 100644 analysis/preview/preview_rot270_v5.png create mode 100644 analysis/preview/preview_rot90_v5.png diff --git a/analysis/preview/preview_portrait_v5.png b/analysis/preview/preview_portrait_v5.png new file mode 100644 index 0000000..f1f2c1f --- /dev/null +++ b/analysis/preview/preview_portrait_v5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3729c85023ad2db4d9dbcf3dbb137e4978d15b72979b9428e4bf20b9023496b +size 767935 diff --git a/analysis/preview/preview_rot0_v5.png b/analysis/preview/preview_rot0_v5.png new file mode 100644 index 0000000..f1f2c1f --- /dev/null +++ b/analysis/preview/preview_rot0_v5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3729c85023ad2db4d9dbcf3dbb137e4978d15b72979b9428e4bf20b9023496b +size 767935 diff --git a/analysis/preview/preview_rot180_v5.png b/analysis/preview/preview_rot180_v5.png new file mode 100644 index 0000000..f1f2c1f --- /dev/null +++ b/analysis/preview/preview_rot180_v5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3729c85023ad2db4d9dbcf3dbb137e4978d15b72979b9428e4bf20b9023496b +size 767935 diff --git a/analysis/preview/preview_rot270_v5.png b/analysis/preview/preview_rot270_v5.png new file mode 100644 index 0000000..f1f2c1f --- /dev/null +++ b/analysis/preview/preview_rot270_v5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3729c85023ad2db4d9dbcf3dbb137e4978d15b72979b9428e4bf20b9023496b +size 767935 diff --git a/analysis/preview/preview_rot90_v5.png b/analysis/preview/preview_rot90_v5.png new file mode 100644 index 0000000..f1f2c1f --- /dev/null +++ b/analysis/preview/preview_rot90_v5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3729c85023ad2db4d9dbcf3dbb137e4978d15b72979b9428e4bf20b9023496b +size 767935 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 571cb4e..710d666 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/AppRoot.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/AppRoot.kt index 0db8475..1052ce5 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/ui/AppRoot.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/AppRoot.kt @@ -36,9 +36,10 @@ private val TABS = listOf( ) /** - * App shell. The bottom navigation bar is always at the bottom in both - * orientations (top/bottom bars keep their positions; only their inner - * icons/text follow the screen). Tab content lives in a stable slot. + * App shell. The activity is portrait-locked, so the bottom navigation bar + * is glued to the phone's portrait bottom edge at all times (its absolute + * position never moves, however the phone is physically held). Tab content + * lives in a stable slot. */ @Composable fun AppRoot() { @@ -58,7 +59,7 @@ fun AppRoot() { } } - // bottom navigation overlay (both orientations) + // bottom navigation overlay (glued to the portrait bottom edge) Surface( color = MaterialTheme.colorScheme.surface, modifier = Modifier diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/UiInsets.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/UiInsets.kt index 1ea7c10..b44252d 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/ui/UiInsets.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/UiInsets.kt @@ -2,7 +2,7 @@ package com.mag160c.thermal.ui /** Shared UI inset state (px), written by AppRoot's nav overlay. */ object UiInsets { - /** Bottom navigation overlay height in px (0 in landscape). */ + /** Bottom navigation overlay height in px (portrait-locked app: constant). */ @Volatile var navPx: Int = 0 } diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/gallery/GalleryScreen.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/gallery/GalleryScreen.kt index 60393eb..f388ba3 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/ui/gallery/GalleryScreen.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/gallery/GalleryScreen.kt @@ -63,7 +63,7 @@ fun GalleryScreen(vm: GalleryViewModel = viewModel()) { Text("媒体库 (${items.size})", style = MaterialTheme.typography.titleMedium) Button(onClick = { vm.refresh() }) { Text("刷新") } } - LazyVerticalGrid(columns = GridCells.Fixed(if (isLandscape()) 4 else 3)) { + LazyVerticalGrid(columns = GridCells.Fixed(3)) { items(items.size) { idx -> val item = items[idx] var bmp by remember(item.name) { mutableStateOf(null) } @@ -109,8 +109,3 @@ fun GalleryScreen(vm: GalleryViewModel = viewModel()) { } } } - -@Composable -private fun isLandscape(): Boolean = - androidx.compose.ui.platform.LocalConfiguration.current.orientation == - android.content.res.Configuration.ORIENTATION_LANDSCAPE diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveRenderer.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveRenderer.kt index 8426045..8fff707 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveRenderer.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveRenderer.kt @@ -11,11 +11,13 @@ import android.view.SurfaceView /** * Software canvas renderer for the live IR stream. * - * The thermal image is ALWAYS drawn rotated 90 deg CW (3:4 vertical) into a - * fixed fitted rect inside the available area (full screen minus the control - * top bar and the bottom navigation bar). The image region does NOT move or - * rotate with the phone; only the OSD text stays screen-horizontal so the - * labels are always readable. + * The activity is portrait-locked, so the composition below is glued to the + * phone's portrait frame: the thermal image is ALWAYS drawn rotated 90 deg + * CW (3:4 vertical) into a fixed fitted rect inside the available area + * (full screen minus the control top bar and the bottom navigation bar). + * The image region does NOT move or rotate however the phone is physically + * held, so the on-screen area always matches the thermal lens direction; + * OSD text stays screen-horizontal so the labels are always readable. */ class LiveRenderer( private val surfaceView: SurfaceView, diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveScreen.kt b/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveScreen.kt index d409be2..a5b4450 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveScreen.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/ui/live/LiveScreen.kt @@ -43,10 +43,12 @@ import com.mag160c.thermal.core.Palettes import android.view.SurfaceView /** - * Stable single-branch live view: one SurfaceView for the whole screen, - * a control TOP bar (always visible) and overlays anchored by orientation. - * Image content stays world-fixed across rotations (renderer handles the - * draw rotation from the display rotation). + * Stable single-branch live view: one SurfaceView for the whole screen, a + * control TOP bar (always visible) and the bottom navigation overlay. + * The activity is portrait-locked, so the whole composition (top bar / + * image region / bottom bar) is glued to the phone's portrait frame and + * never moves or rotates, however the phone is physically held — the on- + * screen area always matches the thermal lens direction. */ @Composable fun LiveScreen(vm: LiveViewModel = viewModel()) { @@ -86,7 +88,7 @@ fun LiveScreen(vm: LiveViewModel = viewModel()) { ) } - // ---- control TOP bar (always at top, both orientations) ---- + // ---- control TOP bar (glued to the portrait top edge) ---- Surface( color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier diff --git a/build-artifacts/mag160c-app-debug.apk b/build-artifacts/mag160c-app-debug.apk index 5f26c42..564ffec 100644 --- a/build-artifacts/mag160c-app-debug.apk +++ b/build-artifacts/mag160c-app-debug.apk @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf2b7c9cd468d32ab443ebb54213ee0dc9744cd1f136342a4769f191c7955277 +oid sha256:900c58d4d8138199318304a6afa35f66b47e2c982b391a6d826281e525654a39 size 11806159 diff --git a/docs/android_app/HANDOFF_DEVELOPMENT.md b/docs/android_app/HANDOFF_DEVELOPMENT.md index 0c1302d..8c46639 100644 --- a/docs/android_app/HANDOFF_DEVELOPMENT.md +++ b/docs/android_app/HANDOFF_DEVELOPMENT.md @@ -22,7 +22,7 @@ - 适配 **Android 16(API 36)**;用户反馈旧专业版在 Android 16 上花屏,重写根治。 - 逆向证据:`docs/android_app/reverse_apk_features.md`(4 APP 功能总表)、 `analysis/protocol_spec.md`(USB 协议/温度算法全逆向)。 -- 用户要求:现代 UI、横竖屏自适应、**不建立流氓文件夹**(媒体→MediaStore DCIM/MAG160C)、 +- 用户要求:现代 UI、**构图钉死竖屏框架**(竖屏锁定,见 §4.3)、**不建立流氓文件夹**(媒体→MediaStore DCIM/MAG160C)、 无 32 位库(纯 Kotlin,无 NDK)。 ## 2. 技术栈与工具链(已部署) @@ -50,13 +50,13 @@ ``` android/app/src/main/kotlin/com/mag160c/thermal/ ├─ MainActivity.kt 启动,enableEdgeToEdge + 隐藏状态栏(沉浸) -├─ ui/AppRoot.kt App壳:底部导航(实时/相册/分析/设置)恒在底部(横竖屏一致) +├─ ui/AppRoot.kt App壳:底部导航(实时/相册/分析/设置)恒贴竖屏底边(Activity 竖屏锁定) ├─ ui/UiInsets.kt 单例:底部导航高度 px(渲染器用它留白) ├─ ui/theme/Theme.kt M3 动态取色(Android12+) ├─ ui/live/ ★实时画面(最核心) │ ├─ LiveViewModel.kt 状态机/演示模式/多点测温/追踪开关/拍照/录像/inset上报 │ ├─ LiveScreen.kt 稳定单分支布局:顶栏控制条(恒顶部,适配挖孔屏)+SurfaceView -│ └─ LiveRenderer.kt SurfaceView 软件渲染:图像恒90°旋转(3:4竖)固定不随手机转, +│ └─ LiveRenderer.kt SurfaceView 软件渲染:图像恒90°旋转(3:4竖)钉在竖屏框架固定区域, │ 文字恒屏幕水平;色标条/圆圈标记/中心温OSD ├─ ui/gallery/ 相册页(MediaStore DCIM/MAG160C 扫描 + 内嵌JPEG缩略图 + 运行时媒体权限) ├─ ui/analyze/ MDT 离线分析(缩放/调色板重渲染/备注回写/PDF报告) @@ -100,13 +100,17 @@ res/drawable/*.xml 自绘矢量图标(双弧圆等可靠几何图形 - 帧流:EP 0x81 bulk,0x1BB1B11B 头 + 0x1C 起像素 + 尾部 shutter 于 +0x24。 - 相机信息块 0x5BB5B55B:+0x00 pid、+0x08 序列号、+0x10 宽、+0x14 高、+0x18 fps。 -### 4.3 实时画面渲染(本轮设计约定) -- **图像恒旋转90°CW绘制为 3:4 竖向**,填满可用区域(顶栏下~底导航上), - **不随手机旋转**(手机怎么转图像区域都不动)。 +### 4.3 实时画面渲染(最终约定:构图钉死竖屏框架) +- **Activity 锁定竖屏**(manifest `screenOrientation="portrait"`):屏幕相对手机框架 + 永不旋转。顶栏、热像区域、底栏的**绝对位置**永远贴着手机竖屏的物理顶边(挖孔侧)、 + 中间、物理底边;手机怎么物理旋转,构图都不动(用户明确要求:屏显方向必须始终 + 与热像镜头实际方向对应,横屏后显示区域跟着屏幕转是错的)。**不要改回 fullSensor。** +- **图像恒 90°CW 绘制为 3:4 竖向**,填满可用区域(顶栏下~底导航上)。 - **文字/图标恒屏幕水平**(可读);标记文字位置自动跟随(probeToScreen 固定 90° 映射: `fx=1-sy/120, fy=sx/160`)。 -- 顶栏=控制项(FFC/变倍/追踪·开/调色板/拍照/录像/调色板名),恒在顶部,`safeDrawing` 顶部inset 适配挖孔屏。 -- 底栏=导航恒在底部(横竖屏一致;**没有横屏左栏**)。 +- 顶栏=控制项(FFC/变倍/追踪·开/调色板/拍照/录像/调色板名),恒在顶部,`safeDrawing` + 顶部inset 适配挖孔屏。 +- 底栏=导航恒在底部。 - 顶栏高度经 `onSizeChanged`→`vm.uiTopPx`;底导航高度经 `UiInsets.navPx`→`vm.uiBottomPx`,渲染器据此留白。 - 演示模式:无设备时用真实管线+DDT渲染合成帧(热块场景),demo温度用局部线性显示映射 (真机温度走管线数学;绝对温度标定待真机)。 diff --git a/docs/android_app/session_state.md b/docs/android_app/session_state.md index 0643db2..f77c820 100644 --- a/docs/android_app/session_state.md +++ b/docs/android_app/session_state.md @@ -121,6 +121,23 @@ 用固定 90° 映射) - [x] 完整交接文档:docs/android_app/HANDOFF_DEVELOPMENT.md(含给新模型的读取提示词) +## 用户反馈修复 第五轮(2026-09-07,布局约定最终定稿:构图钉死竖屏框架) + +- [x] **用户澄清**:"顶栏底栏位置不变"指**绝对位置**——永远贴着手机竖屏的物理顶边 + (挖孔侧)与物理底边;热像区域也**完全不动**。不是"横屏后显示区域跟着屏幕转": + 那样屏显方向会和镜头实际方向对不上。 +- [x] **根因与修复**:此前 fullSensor 下渲染"相对当前屏幕固定 90°CW",手机横过来时 + 屏幕本身转 90°,整个构图相对手机框架也转 90° → 与镜头方向脱节。修复 = + **Activity 锁定竖屏**(manifest `screenOrientation="portrait"`):屏幕相对手机框架 + 永不旋转,构图(顶栏/热像区/底栏)永远钉在竖屏框架上;手机怎么物理旋转都一样。 +- [x] 顺带清理:GalleryScreen 去掉 isLandscape() 死逻辑(恒 3 列); + AppRoot/LiveScreen/LiveRenderer/UiInsets 注释同步更新。 +- [x] 模拟器四方向实测(0/90/180/270°,`adb emu rotate`):**四个方向截图逐字节一致** + (MD5 相同)——构图绝对固定;演示画面/顶栏/底栏/色标/标记/中文均正常 + (analysis/preview/preview_rot*_v5.png)。 +- [x] 单元测试全过;APK 已更新 build-artifacts/mag160c-app-debug.apk。 +- 注:不要再改回 fullSensor/sensor 横屏;横竖屏适配类需求一律以"竖屏构图恒定"为准。 + ## 待办 - 真机USB实测(温度绝对值标定、FFC/录像/MDT保存端到端)