android: lock portrait - UI composition glued to phone frame (top/bottom bars + image area absolutely fixed)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,7 +16,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:screenOrientation="fullSensor"
|
android:screenOrientation="portrait"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||||
android:resizeableActivity="true">
|
android:resizeableActivity="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ private val TABS = listOf(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App shell. The bottom navigation bar is always at the bottom in both
|
* App shell. The activity is portrait-locked, so the bottom navigation bar
|
||||||
* orientations (top/bottom bars keep their positions; only their inner
|
* is glued to the phone's portrait bottom edge at all times (its absolute
|
||||||
* icons/text follow the screen). Tab content lives in a stable slot.
|
* position never moves, however the phone is physically held). Tab content
|
||||||
|
* lives in a stable slot.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AppRoot() {
|
fun AppRoot() {
|
||||||
@@ -58,7 +59,7 @@ fun AppRoot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bottom navigation overlay (both orientations)
|
// bottom navigation overlay (glued to the portrait bottom edge)
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.mag160c.thermal.ui
|
|||||||
|
|
||||||
/** Shared UI inset state (px), written by AppRoot's nav overlay. */
|
/** Shared UI inset state (px), written by AppRoot's nav overlay. */
|
||||||
object UiInsets {
|
object UiInsets {
|
||||||
/** Bottom navigation overlay height in px (0 in landscape). */
|
/** Bottom navigation overlay height in px (portrait-locked app: constant). */
|
||||||
@Volatile
|
@Volatile
|
||||||
var navPx: Int = 0
|
var navPx: Int = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ fun GalleryScreen(vm: GalleryViewModel = viewModel()) {
|
|||||||
Text("媒体库 (${items.size})", style = MaterialTheme.typography.titleMedium)
|
Text("媒体库 (${items.size})", style = MaterialTheme.typography.titleMedium)
|
||||||
Button(onClick = { vm.refresh() }) { Text("刷新") }
|
Button(onClick = { vm.refresh() }) { Text("刷新") }
|
||||||
}
|
}
|
||||||
LazyVerticalGrid(columns = GridCells.Fixed(if (isLandscape()) 4 else 3)) {
|
LazyVerticalGrid(columns = GridCells.Fixed(3)) {
|
||||||
items(items.size) { idx ->
|
items(items.size) { idx ->
|
||||||
val item = items[idx]
|
val item = items[idx]
|
||||||
var bmp by remember(item.name) { mutableStateOf<android.graphics.Bitmap?>(null) }
|
var bmp by remember(item.name) { mutableStateOf<android.graphics.Bitmap?>(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
|
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import android.view.SurfaceView
|
|||||||
/**
|
/**
|
||||||
* Software canvas renderer for the live IR stream.
|
* Software canvas renderer for the live IR stream.
|
||||||
*
|
*
|
||||||
* The thermal image is ALWAYS drawn rotated 90 deg CW (3:4 vertical) into a
|
* The activity is portrait-locked, so the composition below is glued to the
|
||||||
* fixed fitted rect inside the available area (full screen minus the control
|
* phone's portrait frame: the thermal image is ALWAYS drawn rotated 90 deg
|
||||||
* top bar and the bottom navigation bar). The image region does NOT move or
|
* CW (3:4 vertical) into a fixed fitted rect inside the available area
|
||||||
* rotate with the phone; only the OSD text stays screen-horizontal so the
|
* (full screen minus the control top bar and the bottom navigation bar).
|
||||||
* labels are always readable.
|
* 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(
|
class LiveRenderer(
|
||||||
private val surfaceView: SurfaceView,
|
private val surfaceView: SurfaceView,
|
||||||
|
|||||||
@@ -43,10 +43,12 @@ import com.mag160c.thermal.core.Palettes
|
|||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stable single-branch live view: one SurfaceView for the whole screen,
|
* Stable single-branch live view: one SurfaceView for the whole screen, a
|
||||||
* a control TOP bar (always visible) and overlays anchored by orientation.
|
* control TOP bar (always visible) and the bottom navigation overlay.
|
||||||
* Image content stays world-fixed across rotations (renderer handles the
|
* The activity is portrait-locked, so the whole composition (top bar /
|
||||||
* draw rotation from the display rotation).
|
* 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
|
@Composable
|
||||||
fun LiveScreen(vm: LiveViewModel = viewModel()) {
|
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(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
Binary file not shown.
@@ -22,7 +22,7 @@
|
|||||||
- 适配 **Android 16(API 36)**;用户反馈旧专业版在 Android 16 上花屏,重写根治。
|
- 适配 **Android 16(API 36)**;用户反馈旧专业版在 Android 16 上花屏,重写根治。
|
||||||
- 逆向证据:`docs/android_app/reverse_apk_features.md`(4 APP 功能总表)、
|
- 逆向证据:`docs/android_app/reverse_apk_features.md`(4 APP 功能总表)、
|
||||||
`analysis/protocol_spec.md`(USB 协议/温度算法全逆向)。
|
`analysis/protocol_spec.md`(USB 协议/温度算法全逆向)。
|
||||||
- 用户要求:现代 UI、横竖屏自适应、**不建立流氓文件夹**(媒体→MediaStore DCIM/MAG160C)、
|
- 用户要求:现代 UI、**构图钉死竖屏框架**(竖屏锁定,见 §4.3)、**不建立流氓文件夹**(媒体→MediaStore DCIM/MAG160C)、
|
||||||
无 32 位库(纯 Kotlin,无 NDK)。
|
无 32 位库(纯 Kotlin,无 NDK)。
|
||||||
|
|
||||||
## 2. 技术栈与工具链(已部署)
|
## 2. 技术栈与工具链(已部署)
|
||||||
@@ -50,13 +50,13 @@
|
|||||||
```
|
```
|
||||||
android/app/src/main/kotlin/com/mag160c/thermal/
|
android/app/src/main/kotlin/com/mag160c/thermal/
|
||||||
├─ MainActivity.kt 启动,enableEdgeToEdge + 隐藏状态栏(沉浸)
|
├─ MainActivity.kt 启动,enableEdgeToEdge + 隐藏状态栏(沉浸)
|
||||||
├─ ui/AppRoot.kt App壳:底部导航(实时/相册/分析/设置)恒在底部(横竖屏一致)
|
├─ ui/AppRoot.kt App壳:底部导航(实时/相册/分析/设置)恒贴竖屏底边(Activity 竖屏锁定)
|
||||||
├─ ui/UiInsets.kt 单例:底部导航高度 px(渲染器用它留白)
|
├─ ui/UiInsets.kt 单例:底部导航高度 px(渲染器用它留白)
|
||||||
├─ ui/theme/Theme.kt M3 动态取色(Android12+)
|
├─ ui/theme/Theme.kt M3 动态取色(Android12+)
|
||||||
├─ ui/live/ ★实时画面(最核心)
|
├─ ui/live/ ★实时画面(最核心)
|
||||||
│ ├─ LiveViewModel.kt 状态机/演示模式/多点测温/追踪开关/拍照/录像/inset上报
|
│ ├─ LiveViewModel.kt 状态机/演示模式/多点测温/追踪开关/拍照/录像/inset上报
|
||||||
│ ├─ LiveScreen.kt 稳定单分支布局:顶栏控制条(恒顶部,适配挖孔屏)+SurfaceView
|
│ ├─ LiveScreen.kt 稳定单分支布局:顶栏控制条(恒顶部,适配挖孔屏)+SurfaceView
|
||||||
│ └─ LiveRenderer.kt SurfaceView 软件渲染:图像恒90°旋转(3:4竖)固定不随手机转,
|
│ └─ LiveRenderer.kt SurfaceView 软件渲染:图像恒90°旋转(3:4竖)钉在竖屏框架固定区域,
|
||||||
│ 文字恒屏幕水平;色标条/圆圈标记/中心温OSD
|
│ 文字恒屏幕水平;色标条/圆圈标记/中心温OSD
|
||||||
├─ ui/gallery/ 相册页(MediaStore DCIM/MAG160C 扫描 + 内嵌JPEG缩略图 + 运行时媒体权限)
|
├─ ui/gallery/ 相册页(MediaStore DCIM/MAG160C 扫描 + 内嵌JPEG缩略图 + 运行时媒体权限)
|
||||||
├─ ui/analyze/ MDT 离线分析(缩放/调色板重渲染/备注回写/PDF报告)
|
├─ ui/analyze/ MDT 离线分析(缩放/调色板重渲染/备注回写/PDF报告)
|
||||||
@@ -100,13 +100,17 @@ res/drawable/*.xml 自绘矢量图标(双弧圆等可靠几何图形
|
|||||||
- 帧流:EP 0x81 bulk,0x1BB1B11B 头 + 0x1C 起像素 + 尾部 shutter 于 +0x24。
|
- 帧流:EP 0x81 bulk,0x1BB1B11B 头 + 0x1C 起像素 + 尾部 shutter 于 +0x24。
|
||||||
- 相机信息块 0x5BB5B55B:+0x00 pid、+0x08 序列号、+0x10 宽、+0x14 高、+0x18 fps。
|
- 相机信息块 0x5BB5B55B:+0x00 pid、+0x08 序列号、+0x10 宽、+0x14 高、+0x18 fps。
|
||||||
|
|
||||||
### 4.3 实时画面渲染(本轮设计约定)
|
### 4.3 实时画面渲染(最终约定:构图钉死竖屏框架)
|
||||||
- **图像恒旋转90°CW绘制为 3:4 竖向**,填满可用区域(顶栏下~底导航上),
|
- **Activity 锁定竖屏**(manifest `screenOrientation="portrait"`):屏幕相对手机框架
|
||||||
**不随手机旋转**(手机怎么转图像区域都不动)。
|
永不旋转。顶栏、热像区域、底栏的**绝对位置**永远贴着手机竖屏的物理顶边(挖孔侧)、
|
||||||
|
中间、物理底边;手机怎么物理旋转,构图都不动(用户明确要求:屏显方向必须始终
|
||||||
|
与热像镜头实际方向对应,横屏后显示区域跟着屏幕转是错的)。**不要改回 fullSensor。**
|
||||||
|
- **图像恒 90°CW 绘制为 3:4 竖向**,填满可用区域(顶栏下~底导航上)。
|
||||||
- **文字/图标恒屏幕水平**(可读);标记文字位置自动跟随(probeToScreen 固定 90° 映射:
|
- **文字/图标恒屏幕水平**(可读);标记文字位置自动跟随(probeToScreen 固定 90° 映射:
|
||||||
`fx=1-sy/120, fy=sx/160`)。
|
`fx=1-sy/120, fy=sx/160`)。
|
||||||
- 顶栏=控制项(FFC/变倍/追踪·开/调色板/拍照/录像/调色板名),恒在顶部,`safeDrawing` 顶部inset 适配挖孔屏。
|
- 顶栏=控制项(FFC/变倍/追踪·开/调色板/拍照/录像/调色板名),恒在顶部,`safeDrawing`
|
||||||
- 底栏=导航恒在底部(横竖屏一致;**没有横屏左栏**)。
|
顶部inset 适配挖孔屏。
|
||||||
|
- 底栏=导航恒在底部。
|
||||||
- 顶栏高度经 `onSizeChanged`→`vm.uiTopPx`;底导航高度经 `UiInsets.navPx`→`vm.uiBottomPx`,渲染器据此留白。
|
- 顶栏高度经 `onSizeChanged`→`vm.uiTopPx`;底导航高度经 `UiInsets.navPx`→`vm.uiBottomPx`,渲染器据此留白。
|
||||||
- 演示模式:无设备时用真实管线+DDT渲染合成帧(热块场景),demo温度用局部线性显示映射
|
- 演示模式:无设备时用真实管线+DDT渲染合成帧(热块场景),demo温度用局部线性显示映射
|
||||||
(真机温度走管线数学;绝对温度标定待真机)。
|
(真机温度走管线数学;绝对温度标定待真机)。
|
||||||
|
|||||||
@@ -121,6 +121,23 @@
|
|||||||
用固定 90° 映射)
|
用固定 90° 映射)
|
||||||
- [x] 完整交接文档:docs/android_app/HANDOFF_DEVELOPMENT.md(含给新模型的读取提示词)
|
- [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保存端到端)
|
- 真机USB实测(温度绝对值标定、FFC/录像/MDT保存端到端)
|
||||||
|
|||||||
Reference in New Issue
Block a user