android: lock portrait - UI composition glued to phone frame (top/bottom bars + image area absolutely fixed)

This commit is contained in:
ZXCLI
2026-09-07 13:17:19 +08:00
parent 2c38537303
commit d0b133a895
14 changed files with 67 additions and 31 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="fullSensor"
android:screenOrientation="portrait"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:resizeableActivity="true">
<intent-filter>
@@ -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
@@ -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
}
@@ -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<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.
*
* 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,
@@ -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