外观
相机旋转处理规范
1. 背景
- App 整体锁定竖屏:
android:screenOrientation="portrait" - 相机预览自动旋转:CameraX 根据传感器自动调整预览方向
- UI 元素需要手动旋转:水印、操作栏、缩放条等需根据设备物理方向同步旋转
2. 核心原则
| 原则 | 说明 |
|---|---|
| 传感器驱动 | 使用 OrientationEventListener 获取设备真实物理方向,不受 Activity 锁定影响 |
| 统一接口 | 提供单一入口获取旋转状态,所有相机功能复用 |
| 方向组合 | 通过水平/垂直方向组合确定元素位置,灵活适配各种场景 |
| 灵活尺寸 | 支持固定值、比例、透明度等多种配置方式 |
| 块状组合 | 将多个需要同步旋转的元素组合成块,统一管理 |
3. 方向组合定义
kotlin
// 水平方向
enum class HorizontalAlignment {
START, // 左
CENTER, // 中
END // 右
}
// 垂直方向
enum class VerticalAlignment {
TOP, // 上
CENTER, // 中
BOTTOM // 下
}
// 旋转策略
enum class RotationStrategy {
SYNC_ROTATE, // 同步旋转(与预览方向一致)
FIXED_POSITION, // 固定位置(始终使用 rotation0 对齐,不随物理旋转移动)
REVERSE_COMPENSATE // 反向补偿(内容反向旋转,特殊场景)
}4. 方向组合映射
根据水平和垂直方向的组合,自动计算各旋转状态下的对齐方式。
映射原理:Activity 锁定竖屏,Compose 坐标系不随设备旋转变化。根据物理旋转方向映射 Compose Alignment,使元素始终位于用户视角的同一位置。
以 START + BOTTOM = 视觉"左下"为例(逆时针旋转 CCW → ROTATION_90):
- 竖屏时 Compose Top = 用户上方,Compose Start = 用户左侧
- 逆时针旋转 90° 后:Compose Top = 用户左侧,Compose Start = 用户底部
- 需要
TopStart才能出现在用户的左下角
kotlin
fun getPositionMapping(horizontal: HorizontalAlignment, vertical: VerticalAlignment): PositionMapping {
// 映射规则(以 START+BOTTOM = 视觉"左下"为例):
// ROTATION_0 (竖屏): 不反转 → BottomStart = 左下 ✓
// ROTATION_90 (CCW): 只反转垂直 → TopStart = 左下 ✓
// ROTATION_180 (倒置): 反转水平+垂直 → TopEnd = 左下 ✓
// ROTATION_270 (CW): 只反转水平 → BottomEnd = 左下 ✓
return PositionMapping(
rotation0 = mapToAlignment(horizontal, vertical),
rotation90 = mapToAlignment(horizontal, invertVertical(vertical)),
rotation180 = mapToAlignment(invertHorizontal(horizontal), invertVertical(vertical)),
rotation270 = mapToAlignment(invertHorizontal(horizontal), vertical)
)
}
private fun invertHorizontal(horizontal: HorizontalAlignment): HorizontalAlignment {
return when (horizontal) {
HorizontalAlignment.START -> HorizontalAlignment.END
HorizontalAlignment.CENTER -> HorizontalAlignment.CENTER
HorizontalAlignment.END -> HorizontalAlignment.START
}
}
private fun invertVertical(vertical: VerticalAlignment): VerticalAlignment {
return when (vertical) {
VerticalAlignment.TOP -> VerticalAlignment.BOTTOM
VerticalAlignment.CENTER -> VerticalAlignment.CENTER
VerticalAlignment.BOTTOM -> VerticalAlignment.TOP
}
}
data class PositionMapping(
val rotation0: Alignment,
val rotation90: Alignment,
val rotation180: Alignment,
val rotation270: Alignment
)5. 旋转块配置
kotlin
data class RotationBlockConfig(
val strategy: RotationStrategy = RotationStrategy.SYNC_ROTATE,
val horizontal: HorizontalAlignment = HorizontalAlignment.CENTER,
val vertical: VerticalAlignment = VerticalAlignment.CENTER,
// 尺寸配置
val widthPercent: Float? = null,
val heightPercent: Float? = null,
val fixedWidth: Dp? = null,
val fixedHeight: Dp? = null,
val maxWidth: Dp? = null,
val maxHeight: Dp? = null,
val aspectRatio: Float? = null,
// 视觉配置
val alpha: Float? = null,
val cornerRadius: Dp? = null,
val padding: PaddingValues = PaddingValues(0.dp),
val zIndex: Float? = null,
)6. 旋转块组合函数
kotlin
@Composable
fun RotationBlock(
config: RotationBlockConfig,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val screenRotation = rememberScreenRotation()
val rotationAngle = getRotationAngle(screenRotation.value)
val isLandscape = isLandscape(screenRotation.value)
val positionMapping = getPositionMapping(config.horizontal, config.vertical)
val alignment = when (screenRotation.value) {
Surface.ROTATION_0 -> positionMapping.rotation0
Surface.ROTATION_90 -> positionMapping.rotation90
Surface.ROTATION_180 -> positionMapping.rotation180
Surface.ROTATION_270 -> positionMapping.rotation270
else -> positionMapping.rotation0
}
val swapDimensions = isLandscape && config.strategy != RotationStrategy.REVERSE_COMPENSATE
val contentModifier = Modifier
.then(applySizeConstraints(config, swapDimensions))
.then(applyVisualEffects(config))
.padding(config.padding)
.run { config.zIndex?.let { zIndex(it) } ?: this }
when (config.strategy) {
RotationStrategy.SYNC_ROTATE ->
Box(modifier = modifier.fillMaxSize(), contentAlignment = alignment) {
Box(modifier = contentModifier.rotate(rotationAngle)) { content() }
}
RotationStrategy.FIXED_POSITION -> {
val fixedAlignment = getPositionMapping(config.horizontal, config.vertical).rotation0
Box(modifier = modifier.fillMaxSize()) {
Box(modifier = contentModifier.align(fixedAlignment)) { content() }
}
}
RotationStrategy.REVERSE_COMPENSATE ->
Box(modifier = modifier.fillMaxSize(), contentAlignment = alignment) {
Box(modifier = contentModifier.rotate(-rotationAngle)) { content() }
}
}
}注意:RotationBlock 使用 Box(contentAlignment) + Modifier.rotate() 方式定位。该方式基于未旋转的 bounds 定位,旋转后内容围绕中心点旋转,对于矩形内容会有视觉偏移。对于需要精确角落定位的矩形内容(如信息水印、CameraAdjustmentOverlay),应使用第 14 节的 RotatedCornerContent。
7. 位置自适应
7.1 方向组合示例
| 水平 | 垂直 | 组合结果 |
|---|---|---|
| START | TOP | 左上角 |
| END | BOTTOM | 右下角 |
| CENTER | CENTER | 居中 |
7.2 自适应规则汇总
| 设备方向 | START+BOTTOM | END+BOTTOM | CENTER+CENTER |
|---|---|---|---|
| 竖屏 (0°) | 左下角 | 右下角 | 居中 |
| 横屏右 (90°) | 左上角 | 左下角 | 居中 |
| 倒置 (180°) | 右上角 | 左上角 | 居中 |
| 横屏左 (270°) | 右下角 | 右上角 | 居中 |
8. 尺寸约束
8.1 尺寸约束优先级
fixedWidth/fixedHeight → fillMaxWidth/fillMaxHeight → sizeIn → aspectRatio → alpha → clip → padding → rotate
8.2 通用尺寸常量
kotlin
object RotationSizes {
// 比例
const val WIDTH_SMALL = 0.33f
const val WIDTH_MEDIUM = 0.5f
const val WIDTH_LARGE = 0.666f
const val WIDTH_FULL = 0.9f
// 固定尺寸
val DP_4 = 4.dp
val DP_8 = 8.dp
val DP_16 = 16.dp
val DP_70 = 70.dp
val DP_140 = 140.dp
// 透明度
const val ALPHA_LOW = 0.2f
const val ALPHA_MEDIUM = 0.5f
}9. 水印相机配置示例
kotlin
@Composable
fun WatermarkCameraOverlay() {
// 现场拍照水印 - 居中(使用 RotationBlock,内容是正方形/居中可视区域)
RotationBlock(
config = RotationBlockConfig(
strategy = RotationStrategy.SYNC_ROTATE,
horizontal = HorizontalAlignment.CENTER,
vertical = VerticalAlignment.CENTER,
widthPercent = RotationSizes.WIDTH_FULL,
heightPercent = 0.3f,
maxWidth = 600.dp,
maxHeight = 200.dp,
aspectRatio = 3.33f,
alpha = RotationSizes.ALPHA_LOW
)
) { /* 内容 */ }
// 信息水印 - 左下角(使用 RotatedCornerContent,矩形内容需精确角落定位)
// 详见第 14 节
val screenRotation = rememberScreenRotation()
val infoAlignment = when (screenRotation.value) {
Surface.ROTATION_0 -> Alignment.BottomStart
Surface.ROTATION_90 -> Alignment.TopStart
Surface.ROTATION_180 -> Alignment.TopEnd
Surface.ROTATION_270 -> Alignment.BottomEnd
else -> Alignment.BottomStart
}
RotatedCornerContent(
rotation = getRotationAngle(screenRotation.value),
alignment = infoAlignment,
) {
Column(modifier = Modifier.padding(start = 12.dp, bottom = 12.dp)) {
/* 水印文本行 */
}
}
// 防伪水印 - 右下角(使用 RotationBlock FIXED_POSITION,不旋转)
RotationBlock(
config = RotationBlockConfig(
strategy = RotationStrategy.FIXED_POSITION,
horizontal = HorizontalAlignment.END,
vertical = VerticalAlignment.BOTTOM,
fixedWidth = 140.dp,
fixedHeight = 70.dp,
alpha = 0.2f
)
) { /* 内容 */ }
}10. 通用 API
| 函数 | 功能 |
|---|---|
rememberScreenRotation() | 监听屏幕旋转状态 |
getRotationAngle(rotation) | 转换为角度值 |
isLandscape(rotation) | 判断横屏状态 |
getPositionMapping(h, v) | 获取位置映射 |
RotatedCornerContent(rotation, alignment) | 旋转后精确角落定位(见第 14 节) |
11. 注意事项
- 尺寸计算顺序:先应用约束,再应用旋转
- 固定尺寸优先于比例尺寸
- 传感器不可用时回退到 display.rotation
- 矩形内容角落定位:
Box(contentAlignment) + Modifier.rotate()会有视觉偏移,需用RotatedCornerContent
12. 统一坐标系设计
12.1 设计原则
所有需要旋转的 UI 元素(水印、缩放条、功能按钮等)共享同一个外层定位容器,外层容固定不变,不旋转、不改变大小,确保坐标系一致:
kotlin
@Composable
fun CameraRotationContainer(
frozen: Boolean = false,
topBarHeight: Dp = 56.dp,
bottomBarHeight: Dp = 80.dp,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
// Activity 锁定竖屏,CameraTopBar 始终在 Compose 顶部,
// bottomBarContent 始终在底部,不随设备旋转变化。
// 始终使用竖屏 padding,避免重复计算顶栏高度。
val paddingValues = PaddingValues(
top = topBarHeight,
bottom = bottomBarHeight
)
Box(modifier = modifier.fillMaxSize().padding(paddingValues)) {
content()
}
}12.2 容器职责
| 职责 | 说明 |
|---|---|
| 全屏坐标系 | 提供统一的定位参考系 |
| 固定栏避让 | 始终使用竖屏 padding(top + bottom),不随旋转变化 |
| 旋转冻结 | frozen 参数为 true 时用于扫码模式冻结旋转 |
| 子元素定位 | 子元素通过 align() 或 RotationBlock/RotatedCornerContent 定位 |
12.3 使用范式
kotlin
CameraRotationContainer(
frozen = selectedTab == "scan",
topBarHeight = if (selectedTab == "watermark") 0.dp else 56.dp,
bottomBarHeight = if (selectedTab == "watermark") 20.dp else 80.dp,
) {
// 现场拍照水印 - 居中(RotationBlock)
RotationBlock(
config = RotationBlockConfig(
strategy = RotationStrategy.SYNC_ROTATE,
horizontal = HorizontalAlignment.CENTER,
vertical = VerticalAlignment.CENTER
)
) { /* 内容 */ }
// 信息水印 - 左下角(RotatedCornerContent,见第 14 节)
RotatedCornerContent(...) { /* 水印文本 */ }
// 防伪水印 - 右下角(RotationBlock FIXED_POSITION)
RotationBlock(
config = RotationBlockConfig(
strategy = RotationStrategy.FIXED_POSITION,
horizontal = HorizontalAlignment.END,
vertical = VerticalAlignment.BOTTOM,
fixedWidth = 140.dp,
fixedHeight = 70.dp,
alpha = 0.2f
)
) { /* 内容 */ }
// 微距切换按钮 - 右上角(RotationBlock)
RotationBlock(
config = RotationBlockConfig(
strategy = RotationStrategy.SYNC_ROTATE,
horizontal = HorizontalAlignment.END,
vertical = VerticalAlignment.TOP,
padding = PaddingValues(top = 8.dp, end = 8.dp)
)
) { /* 微距按钮内容 */ }
}12.4 坐标系一致性保障
| 元素类型 | 实现方式 | 对齐方式 | 坐标系 |
|---|---|---|---|
| 现场拍照水印 | RotationBlock(SYNC_ROTATE) | CENTER | 统一容器 |
| 信息水印 | RotatedCornerContent | 动态映射(见第 14 节) | 统一容器 |
| 防伪水印 | RotationBlock(FIXED_POSITION) | END+BOTTOM | 统一容器 |
| 微距切换按钮 | RotationBlock(SYNC_ROTATE) | END+TOP | 统一容器 |
| CameraAdjustmentOverlay(水印) | RotatedCornerContent | 动态映射(见第 15 节) | CameraScreen 层 |
| CameraAdjustmentOverlay(扫码) | Box(align=TopStart) 不旋转 | TopStart | CameraScreen 层 |
| 缩放条(水印) | 动态 Alignment + vertical 映射 | 见第 13 节 | CameraScreen 层 |
注意:以下元素不放在 CameraRotationContainer 内:
- 缩放条(ZoomControlBar):原因、定位方式、映射规则见第 13 节
- CameraAdjustmentOverlay:在 CameraScreen 层单独管理,水印模式用
RotatedCornerContent旋转,扫码模式不旋转(见第 15 节)
12.5 注意事项
- 单一容器原则:除缩放条和 CameraAdjustmentOverlay 外,旋转敏感元素应放在同一个
CameraRotationContainer内 - 对齐方式统一:使用
Alignment枚举确保定位一致性 - zIndex 管理:根据层级需求设置合理的 zIndex 值
- 尺寸约束:横屏时自动交换宽高约束,确保视觉比例一致
- padding 始终竖屏:
CameraRotationContainer始终使用竖屏 padding(top + bottom),不随旋转变化
12.6 固定栏避让
CameraRotationContainer 通过 topBarHeight / bottomBarHeight 参数配置竖屏 padding 避开固定栏:
kotlin
// 水印模式:CameraTopBar 在 PageTemplate.titleBar 槽位,不在 content 内,
// topBarHeight=0.dp 避免旋转后变成左侧间隙;bottomBarHeight=20.dp 避开底部操作栏
CameraRotationContainer(
frozen = false,
topBarHeight = 0.dp,
bottomBarHeight = 20.dp,
)
// 扫码模式:CameraTopBar 在 content 顶部,需要 topBarHeight=56.dp 避让
CameraRotationContainer(
frozen = true,
topBarHeight = 56.dp,
bottomBarHeight = 80.dp,
)固定栏布局结构:
| 模式 | topBarHeight | bottomBarHeight | 说明 |
|---|---|---|---|
| 水印 | 0.dp | 20.dp | CameraTopBar 在 PageTemplate.titleBar 槽位,不在 content 内 |
| 扫码 | 56.dp | 80.dp | CameraTopBar 在 content 顶部,需避让 |
布局结构示例:
| 层级 | 组件 | 旋转特性 |
|---|---|---|
| 顶部(titleBar 槽位) | CameraTopBar(水印模式) | 固定位置,不旋转 |
| 内容顶部(content 内) | CameraTopBar(扫码模式) | 固定位置,不旋转 |
| 内容 | CameraRotationContainer | 旋转敏感区域 |
| 底部(bottomBarContent 槽位) | CameraTabBar + BottomBar | 固定位置,不旋转 |
13. 缩放条(ZoomControlBar)特殊处理
13.1 背景
缩放条(ZoomControlBar)与普通旋转元素不同,需要保持垂直竖条形态不变,但位置随物理旋转动态移动,始终贴住物理屏幕的右边界。
13.2 为什么不能放在 CameraRotationContainer 内
| 原因 | 说明 |
|---|---|
| 容器 padding 干扰 | CameraRotationContainer 的 padding 用于避让固定栏,会推离右边界 |
| 旋转策略冲突 | SYNC_ROTATE 会旋转内容本身(竖条变横条),FIXED_POSITION 不改变位置,二者都不符合需求 |
13.3 解决方案
缩放条放在 CameraRotationContainer 外部(CameraScreen 层),使用 rememberScreenRotation() 动态映射对齐方式和方向(不使用 Modifier.rotate):
kotlin
// CameraScreen.kt — 缩放控件(CameraRotationContainer 外部)
// 右侧纵向缩放条,Activity 锁定竖屏,
// 根据物理旋转动态映射对齐方式和方向,始终贴物理右边界且保持竖条:
// ROTATION_0 → CenterEnd, vertical=true (物理右=竖屏右, Column竖着)
// ROTATION_90 → BottomCenter, vertical=false(物理右=竖屏底, Row横着在物理中竖起来)
// ROTATION_180 → CenterStart, vertical=true (物理右=竖屏左, Column竖着)
// ROTATION_270 → TopCenter, vertical=false(物理右=竖屏顶, Row横着在物理中竖起来)
val watermarkScreenRotation = rememberScreenRotation()
val watermarkAlignment = when (watermarkScreenRotation.value) {
android.view.Surface.ROTATION_90 -> Alignment.BottomCenter
android.view.Surface.ROTATION_180 -> Alignment.CenterStart
android.view.Surface.ROTATION_270 -> Alignment.TopCenter
else -> Alignment.CenterEnd
}
val watermarkVertical = when (watermarkScreenRotation.value) {
android.view.Surface.ROTATION_90 -> false
android.view.Surface.ROTATION_270 -> false
else -> true
}
ZoomControlBar(
currentZoom = zoomController.currentZoom,
opticalZooms = zoomController.opticalZooms,
maxZoom = zoomController.maxZoom,
vertical = watermarkVertical,
onZoomChange = { newZoom -> /* ... */ },
modifier = Modifier.align(watermarkAlignment),
)关键点:
- 不使用
Modifier.rotate:通过vertical参数切换 Column/Row 布局,物理世界中始终保持竖条形态 - 动态 Alignment 映射:根据物理旋转把竖条贴到物理右边界对应的 Compose 边界
- ZoomControlBar 位置:从
WatermarkModeScreen.kt移到CameraScreen.kt统一管理
13.4 对齐与方向映射规则
| 物理旋转 | 物理右边界对应竖屏坐标系 | Compose Alignment | vertical 参数 | 原因 |
|---|---|---|---|---|
| 竖屏 0° | 右侧 | CenterEnd | true | Column 竖在右侧 |
| 横屏向左 90° | 底部 | BottomCenter | false | Row 横在底部,物理世界中竖起来 |
| 倒置 180° | 左侧 | CenterStart | true | Column 竖在左侧,物理世界中在右侧 |
| 横屏向右 270° | 顶部 | TopCenter | false | Row 横在顶部,物理世界中竖起来 |
13.5 ZoomControlBar 尺寸
| 模式 | 形态 | 尺寸约束 |
|---|---|---|
| 竖屏(vertical=true) | 纵向 Column | 滑动条高度 heightIn(min = 70.dp, max = 100.dp),宽度 36dp |
| 横屏(vertical=false) | 横向 Column(按钮行 + 横向滑条) | 滑动条宽度 widthIn(min = 100.dp, max = 150.dp) |
13.6 布局结构
PageTemplate
├── titleBar: CameraTopBar ← 水印模式:固定顶部槽位,不旋转
├── content: 相机内容区域
│ ├── CameraContainer ← 相机预览底座(不旋转)
│ ├── CameraRotationContainer ← 旋转敏感区域(水印/微距按钮等)
│ │ ├── WatermarkPreviewOverlay(含 RotatedCornerContent 信息水印)
│ │ └── 微距按钮(RotationBlock)
│ ├── ZoomControlBar (watermark) ← 缩放条,CameraRotationContainer 外部
│ │ 用动态 Alignment + vertical 映射实现贴右边界
│ └── CameraAdjustmentOverlay ← 水印模式:RotatedCornerContent 旋转
│ 扫码模式:Box(align=TopStart) 不旋转
└── bottomBarContent ← 固定底部,不旋转14. RotatedCornerContent — 旋转后精确角落定位
14.1 背景
Box(contentAlignment) + Modifier.rotate() 的定位方式存在偏移问题:
contentAlignment基于未旋转的 bounds 定位- 旋转后内容围绕中心点旋转
- 对于矩形内容(宽高不等),旋转后视觉位置会偏移
信息水印和水印模式 CameraAdjustmentOverlay 都是矩形内容,需要精确角落定位,因此使用 RotatedCornerContent。
14.2 文件路径
feature/camera/src/main/java/com/codenote/feature/camera/rotation/RotatedCornerContent.kt
14.3 实现原理
使用自定义 Layout 在测量阶段计算旋转后的视觉尺寸,正确计算偏移量:
kotlin
@Composable
fun RotatedCornerContent(
rotation: Float, // 旋转角度(0/90/180/270)
alignment: Alignment, // Compose 对齐方式(已根据物理旋转映射)
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
if (measurables.isEmpty()) return@Layout layout(0, 0) {}
val placeable = measurables.first().measure(constraints)
val w = placeable.width
val h = placeable.height
val isRotated = rotation % 180f != 0f
// 旋转 90/270 度后,视觉宽高互换
val visualW = if (isRotated) h else w
val visualH = if (isRotated) w else h
val parentW = constraints.maxWidth
val parentH = constraints.maxHeight
// 根据对齐方式计算旋转后视觉内容的左上角位置
val visualOffset: IntOffset = alignment.align(
size = IntSize(visualW, visualH),
space = IntSize(parentW, parentH),
layoutDirection = layoutDirection,
)
layout(parentW, parentH) {
placeable.placeWithLayer(x = 0, y = 0) {
this.rotationZ = rotation
// 旋转围绕内容中心(w/2, h/2),旋转后视觉左上角在
// (w/2 - visualW/2, h/2 - visualH/2) 处,
// 需平移到 (visualOffset.x, visualOffset.y)
this.translationX = visualOffset.x.toFloat() - (w - visualW) / 2f
this.translationY = visualOffset.y.toFloat() - (h - visualH) / 2f
}
}
}
}14.4 信息水印映射规则
信息水印始终位于用户视角的左下角:
| 物理旋转 | Compose Alignment | 原因 |
|---|---|---|
| ROTATION_0(竖屏) | BottomStart | 竖屏左下角 |
| ROTATION_90(逆时针) | TopStart | Compose 顶部=用户左侧 |
| ROTATION_180(倒置) | TopEnd | Compose 右上=用户左下 |
| ROTATION_270(顺时针) | BottomEnd | Compose 右下=用户左下 |
padding 统一为 start = 12.dp, bottom = 12.dp(在内容 Column 上设置,非 RotatedCornerContent 上)。
14.5 使用示例(信息水印)
kotlin
// WatermarkPreviewOverlay.kt
val screenRotation = rememberScreenRotation()
val infoAlignment = when (screenRotation.value) {
Surface.ROTATION_0 -> Alignment.BottomStart
Surface.ROTATION_90 -> Alignment.TopStart
Surface.ROTATION_180 -> Alignment.TopEnd
Surface.ROTATION_270 -> Alignment.BottomEnd
else -> Alignment.BottomStart
}
RotatedCornerContent(
rotation = getRotationAngle(screenRotation.value),
alignment = infoAlignment,
) {
Column(
modifier = Modifier
.padding(start = 12.dp, bottom = 12.dp)
.clip(RoundedCornerShape(8.dp))
.clickable(onClick = onWatermarkClick),
) {
lines.forEach { line -> Text(text = line, /* ... */) }
}
}15. CameraAdjustmentOverlay 旋转处理
15.1 两种模式差异
| 模式 | 实现方式 | 是否旋转 |
|---|---|---|
| 水印模式 | RotatedCornerContent 包裹 | 旋转,映射到用户视角左上角 |
| 扫码模式 | Box(align=TopStart) 不旋转 | 不旋转,固定左上角 |
15.2 水印模式映射规则
CameraAdjustmentOverlay 始终位于用户视角的左上角:
| 物理旋转 | Compose Alignment | 原因 |
|---|---|---|
| ROTATION_0(竖屏) | TopStart | 竖屏左上角 |
| ROTATION_90(逆时针) | TopEnd | Compose 右上=用户左上 |
| ROTATION_180(倒置) | BottomEnd | Compose 右下=用户左上 |
| ROTATION_270(顺时针) | BottomStart | Compose 左下=用户左上 |
15.3 使用示例(水印模式)
kotlin
// CameraScreen.kt
val screenRotation = rememberScreenRotation()
val rotationAngle = getRotationAngle(screenRotation.value)
val overlayAlignment = when (screenRotation.value) {
android.view.Surface.ROTATION_0 -> Alignment.TopStart
android.view.Surface.ROTATION_90 -> Alignment.TopEnd
android.view.Surface.ROTATION_180 -> Alignment.BottomEnd
android.view.Surface.ROTATION_270 -> Alignment.BottomStart
else -> Alignment.TopStart
}
RotatedCornerContent(
rotation = rotationAngle,
alignment = overlayAlignment,
modifier = Modifier.fillMaxSize(),
) {
CameraAdjustmentOverlay(
/* ... */
modifier = Modifier.padding(top = 8.dp, start = 8.dp),
)
}15.4 使用示例(扫码模式)
kotlin
// CameraScreen.kt — 扫码模式不旋转,固定左上角
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter),
) {
CameraAdjustmentOverlay(
/* ... */
modifier = Modifier
.align(Alignment.TopStart)
.padding(top = 8.dp, start = 8.dp),
)
// 同行右侧:批量/单次切换
ScanModeToggle(/* ... */)
}