Skip to content

相机旋转处理规范

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 方向组合示例

水平垂直组合结果
STARTTOP左上角
ENDBOTTOM右下角
CENTERCENTER居中

7.2 自适应规则汇总

设备方向START+BOTTOMEND+BOTTOMCENTER+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. 注意事项

  1. 尺寸计算顺序:先应用约束,再应用旋转
  2. 固定尺寸优先于比例尺寸
  3. 传感器不可用时回退到 display.rotation
  4. 矩形内容角落定位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) 不旋转TopStartCameraScreen 层
缩放条(水印)动态 Alignment + vertical 映射见第 13 节CameraScreen 层

注意:以下元素不放在 CameraRotationContainer 内:

  • 缩放条(ZoomControlBar):原因、定位方式、映射规则见第 13 节
  • CameraAdjustmentOverlay:在 CameraScreen 层单独管理,水印模式用 RotatedCornerContent 旋转,扫码模式不旋转(见第 15 节)

12.5 注意事项

  1. 单一容器原则:除缩放条和 CameraAdjustmentOverlay 外,旋转敏感元素应放在同一个 CameraRotationContainer
  2. 对齐方式统一:使用 Alignment 枚举确保定位一致性
  3. zIndex 管理:根据层级需求设置合理的 zIndex 值
  4. 尺寸约束:横屏时自动交换宽高约束,确保视觉比例一致
  5. 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,
)

固定栏布局结构

模式topBarHeightbottomBarHeight说明
水印0.dp20.dpCameraTopBar 在 PageTemplate.titleBar 槽位,不在 content 内
扫码56.dp80.dpCameraTopBar 在 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 Alignmentvertical 参数原因
竖屏 0°右侧CenterEndtrueColumn 竖在右侧
横屏向左 90°底部BottomCenterfalseRow 横在底部,物理世界中竖起来
倒置 180°左侧CenterStarttrueColumn 竖在左侧,物理世界中在右侧
横屏向右 270°顶部TopCenterfalseRow 横在顶部,物理世界中竖起来

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(逆时针)TopStartCompose 顶部=用户左侧
ROTATION_180(倒置)TopEndCompose 右上=用户左下
ROTATION_270(顺时针)BottomEndCompose 右下=用户左下

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(逆时针)TopEndCompose 右上=用户左上
ROTATION_180(倒置)BottomEndCompose 右下=用户左上
ROTATION_270(顺时针)BottomStartCompose 左下=用户左上

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(/* ... */)
}