Skip to content

相机功能规范

1. 架构概览

相机功能采用分层架构,将 CameraX 底座、业务模式、UI 组件分离:

PageTemplate
├── titleBar: CameraTopBar              ← 固定顶部,不旋转
├── content: 相机内容区域
│   ├── CameraContainer                 ← CameraX 封装,所有模式共享
│   ├── CameraRotationContainer         ← 旋转敏感区域(水印/扫描框/微距按钮等)
│   │   └── 扫描框/水印/微距按钮等旋转敏感元素
│   ├── ZoomControlBar (watermark)      ← 缩放条,CameraRotationContainer 外部
│   │                                   用动态 Alignment + vertical 映射实现贴右边界
│   └── CameraAdjustmentOverlay         ← 画面调整浮层
│                                        水印模式:RotatedCornerContent 旋转
│                                        扫码模式:Box(align=TopStart) 不旋转
└── bottomBarContent                    ← 固定底部:TabBar + BottomBar

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

  • 缩放条(ZoomControlBar,水印模式):容器 padding 会推离右边界,且旋转策略会改变竖条形态。使用动态 Alignment + vertical 参数映射,不用 Modifier.rotate。详见《/standards/camera-rotation.md》第 13 节。
  • CameraAdjustmentOverlay:水印模式用 RotatedCornerContent 旋转到用户视角左上角,扫码模式用 Box(align=TopStart) 不旋转。详见《/standards/camera-rotation.md》第 15 节。

2. CameraContainer — CameraX 封装

2.1 职责

  • 封装 CameraX 的 Preview/ImageCapture/ImageAnalysis
  • 处理相机权限请求
  • 管理相机状态(闪光灯、对焦、曝光补偿等)
  • 提供相机控制接口(CameraControl)

2.2 状态定义

kotlin
data class CameraContainerState(
    val exposureCompensation: Int = 0,        // 曝光补偿 (-24 ~ +24)
    val whiteBalance: WhiteBalanceMode = WhiteBalanceMode.AUTO,
    val colorEffect: ColorEffect = ColorEffect.NONE,
    val lensFacing: Int = CameraSelector.LENS_FACING_BACK,
    val zoomRatio: Float = 1f,
    val flashOn: Boolean = false,
    val isMacroMode: Boolean = false,
    val macroAvailable: Boolean = false,
    val isSwitchingCamera: Boolean = false,
    val isTorchAuto: Boolean = false,
    val opticalZooms: List<Float> = emptyList(),
    val focusX: Float = 0.5f,                 // 对焦点 x (0~1)
    val focusY: Float = 0.5f,                 // 对焦点 y (0~1)
    val isFocusLocked: Boolean = false,
    val lastLuminosity: Double = 0.0,                // 最新帧亮度
    val cameraStateError: String? = null,            // CameraState 错误消息
)

2.3 调用示例(扫码模式 — 叠加层,不含 CameraContainer)

kotlin
@Composable
fun ScanModeOverlay(
    scanManager: ScanManager,
    onScanResult: (String, ScanResult) -> Unit,
) {
    // 仅业务 UI 叠加层,CameraContainer 在 CameraScreen 层共享
    Box(modifier = Modifier.fillMaxSize()) {
        // 扫描框
        ScanFrameOverlay()

        // 底部操作栏由 CameraScreen 统一渲染
    }
}

2.4 调用示例(水印模式 — 叠加层,不含 CameraContainer)

kotlin
@Composable
fun WatermarkModeScreen(
    cameraState: CameraContainerState,
    onCameraStateChange: (CameraContainerState) -> Unit,
    watermarkViewModel: WatermarkViewModel = hiltViewModel(),
    cameraControl: CameraControl? = null,
    showSettings: Boolean = false,
    onDismissSettings: () -> Unit = {},
) {
    Box(modifier = Modifier.fillMaxSize()) {
        // 水印预览叠加(CameraContainer 在 CameraScreen 层共享)
        // 内部信息水印使用 RotatedCornerContent 旋转,详见旋转规范第 14 节
        WatermarkPreviewOverlay(...)

        // 水印设置弹窗
        WatermarkSettingsSheet(...)

        // 微距切换按钮(RotationBlock SYNC_ROTATE,右上角)
        RotationBlock(
            config = RotationBlockConfig(
                strategy = RotationStrategy.SYNC_ROTATE,
                horizontal = HorizontalAlignment.END,
                vertical = VerticalAlignment.TOP,
                padding = PaddingValues(top = 8.dp, end = 8.dp),
            )
        ) { /* 微距按钮内容 */ }
    }
}

注意

  • 缩放竖条(ZoomControlBar)在 CameraScreen 层统一管理,不在 WatermarkModeScreen 内,避免受 CameraRotationContainer padding 影响。详见《/standards/camera-rotation.md》第 13 节。
  • CameraAdjustmentOverlay 也在 CameraScreen 层管理,不在 WatermarkModeScreen 内。详见《/standards/camera-rotation.md》第 15 节。

3. 新增扫描类型的范式

3.1 新建步骤

  1. feature/camera/ 下新建子目录 xxx/
  2. 创建 XxxModeScreen.kt(组合 CameraContainer + 自定义 UI)
  3. 创建 XxxViewModel.kt(业务后处理逻辑)
  4. CameraScreen.kt 的 Tab 列表加 1 项
  5. 不修改 CameraContainer 的业务分层(仅在基础设施层扩展如能力检测) / NavRoutes / 后端 / 文档

3.2 范式模板

kotlin
// feature/camera/xxx/XxxModeScreen.kt
@Composable
fun XxxModeScreen(
    cameraState: CameraContainerState,
    onCameraStateChange: (CameraContainerState) -> Unit,
    xxxViewModel: XxxViewModel = hiltViewModel(),
    onNavigateToYyy: () -> Unit,
) {
    // 1. CameraX 底座
    CameraContainer(
        state = cameraState,
        onStateChange = onCameraStateChange,
        analyzer = null,                    // 需要分析器则传
        onPhotoCaptured = { bytes ->
            // 拍照后处理
        },
    )

    // 2. 预览叠加层(可选)
    XxxPreviewOverlay(...)

    // 3. 底部操作栏
    XxxBottomBar(...)
}

3.3 约束

可以做的不要做的
使用 CameraContainer 的 onCameraReady 获取 ImageCapture自己创建第二个 CameraX 实例
在 ViewModel 做后处理(压缩/裁剪/保存)在 Screen 里做图像处理
在子 Screen 自定义业务叠加 UI 和底部操作栏修改 CameraContainer 的行为
在子 Screen 叠加自定义预览层在 CameraContainer 里加业务层
使用 CameraScreen 层的 cameraControlRef 控制闪光灯/缩放在子 Screen 内创建 CameraContainer

4. CameraScreen — 入口组合器

4.1 职责

  • 持有 CameraContainerState,各子模式共享
  • 维护当前活跃 Tab,委派给对应子模式
  • 根据入口来源决定初始 Tab(明确指向优先,否则读取历史记录)
  • 底部 TabBar:扫码 / 水印相机 / ...(未来)
  • 缩放控件(扫码模式下底部半透明光焦条 + 纵向滑动展开半圆形拨盘,根据后置摄像头光学倍数动态显示按钮;水印模式下右侧纵向竖条,通过动态 Alignment + vertical 映射始终贴物理右边界,不用 Modifier.rotate
  • CameraAdjustmentOverlay(水印模式用 RotatedCornerContent 旋转到用户视角左上角;扫码模式固定左上角不旋转)
  • CameraState 错误消费(SnackbarHost 展示错误消息后清空 cameraStateError)
  • 缩放值持久化(扫码模式缩放值跨会话记忆)

4.2 入口区分规则

来源参数初始 Tab
首页「二维码扫描」defaultTab="scan"扫码
首页「水印相机」defaultTab="watermark"水印
底部导航「相机」无参数上次使用的 Tab(持久化)
扫码结果页返回无参数上次使用的 Tab(持久化)
相机设置返回无参数上次使用的 Tab(持久化)
其他入口无参数上次使用的 Tab(持久化)

实现方式:

  • 明确入口通过 FormStateManager(策略 A)传递 defaultTab 参数
  • 无参数入口从 CameraMemoryRepository(JSON 文件)读取 lastUsedTab
  • 每次切换 Tab 时自动回写 lastUsedTab 到 JSON 文件

4.3 布局结构

PageTemplate
├── titleBar: CameraTopBar              ← 固定顶部,不旋转
├── content: 相机内容区域
│   ├── CameraContainer                 ← 相机预览底座(不旋转)
│   ├── CameraRotationContainer         ← 旋转敏感区域(始终竖屏 padding,不随旋转变化)
│   │   ├── [扫码] ScanModeOverlay + 扫描线(frozen=true,冻结旋转)
│   │   └── [水印] WatermarkPreviewOverlay(含 RotatedCornerContent 信息水印)+ 微距按钮
│   ├── ZoomControlBar (水印)           ← 缩放条,CameraRotationContainer 外部
│   │                                   根据物理旋转动态映射 Alignment + vertical(不用 rotate)
│   ├── ZoomControlBar (扫码)           ← 底部缩放条,CameraRotationContainer 外部
│   ├── BatchScanFloatingPanel          ← 左侧,固定位置
│   └── CameraAdjustmentOverlay         ← 水印模式:RotatedCornerContent 旋转
│                                        扫码模式:Box(align=TopStart) 不旋转
└── bottomBarContent                    ← 固定底部,不旋转

4.4 旋转区域配置

CameraRotationContainer 始终使用竖屏 padding(top + bottom),不随旋转变化:

kotlin
// 水印模式:CameraTopBar 在 PageTemplate.titleBar 槽位,不在 content 内,
// topBarHeight=0.dp 避免旋转后变成左侧间隙;bottomBarHeight=20.dp 避开底部操作栏
CameraRotationContainer(
    frozen = selectedTab == "scan",                                  // 扫码冻结旋转
    topBarHeight = if (selectedTab == "watermark") 0.dp else 56.dp,
    bottomBarHeight = if (selectedTab == "watermark") 20.dp else 80.dp,
) {
    when (selectedTab) {
        "scan" -> ScanModeOverlay(...)
        "watermark" -> WatermarkModeScreen(...)
    }
}

固定栏避让配置

模式frozentopBarHeightbottomBarHeight说明
水印false0.dp20.dpCameraTopBar 在 PageTemplate.titleBar 槽位,不在 content 内
扫码true56.dp80.dpCameraTopBar 在 content 顶部,需避让;冻结旋转

4.5 伪代码

kotlin
@Composable
fun CameraScreen(
    defaultTab: String? = null,                         // 入口参数,null 时读持久化
    onNavigateToScanResult: (String, ScanResult) -> Unit,
    onNavigateToAlbum: () -> Unit,
    onNavigateToQrCreate: () -> Unit,
    onNavigateToQrList: () -> Unit,
    onNavigateToCameraSettings: () -> Unit,
    viewModel: CameraViewModel = hiltViewModel(),
    watermarkViewModel: WatermarkViewModel = hiltViewModel(),
) {
    val cameraMemory by viewModel.cameraMemory.memory.collectAsState()
    var selectedTab by remember(defaultTab) {
        mutableStateOf(defaultTab ?: "scan")  // 占位,LaunchedEffect 覆盖
    }

    // 初始化记忆 + 从持久化读取 Tab
    LaunchedEffect(Unit) {
        viewModel.cameraMemory.init()
        if (defaultTab == null) {
            selectedTab = cameraMemory.lastUsedTab
        }
    }

    // 切换 Tab 时持久化
    LaunchedEffect(selectedTab) {
        viewModel.cameraMemory.updateLastUsedTab(selectedTab)
    }

    var cameraState by remember { mutableStateOf(CameraContainerState()) }

    PageTemplate(
        titleBar = { CameraTopBar(...) },
        content = {
            // CameraContainer 作为共享底座,放在 when 外部,Tab 切换时不重建
            Box {
                CameraContainer(
                    state = cameraState,
                    onStateChange = { cameraState = it },
                    analyzer = if (selectedTab == "scan") scanAnalyzer else null,
                    onCameraReady = { cameraControl, imageCapture, _ ->
                        // 保存引用供底部操作栏使用
                    },
                )

                // 旋转敏感区域 - 始终使用竖屏 padding(top + bottom),不随旋转变化
                // 水印模式 topBarHeight=0:CameraTopBar 在 PageTemplate.titleBar 槽位,
                // 不在 content 内,无需额外 top padding,否则旋转后变成左侧间隙
                CameraRotationContainer(
                    frozen = selectedTab == "scan",
                    topBarHeight = if (selectedTab == "watermark") 0.dp else 56.dp,
                    bottomBarHeight = if (selectedTab == "watermark") 20.dp else 80.dp,
                ) {
                    when (selectedTab) {
                        "scan" -> ScanModeOverlay(...)
                        "watermark" -> WatermarkModeScreen(...)
                    }
                }

                // 缩放控件 — CameraRotationContainer 外部,避免 padding 影响
                if (selectedTab == "scan") {
                    ZoomControlBar(
                        ...,
                        modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 8.dp)
                    )
                } else if (selectedTab == "watermark") {
                    // 水印模式:根据物理旋转动态映射 Alignment + vertical,不用 Modifier.rotate
                    //   ROTATION_0   → CenterEnd,   vertical=true
                    //   ROTATION_90  → BottomCenter, vertical=false
                    //   ROTATION_180 → CenterStart,  vertical=true
                    //   ROTATION_270 → TopCenter,    vertical=false
                    val watermarkScreenRotation = rememberScreenRotation()
                    val watermarkAlignment = when (watermarkScreenRotation.value) {
                        ROTATION_90 -> Alignment.BottomCenter
                        ROTATION_180 -> Alignment.CenterStart
                        ROTATION_270 -> Alignment.TopCenter
                        else -> Alignment.CenterEnd
                    }
                    val watermarkVertical = when (watermarkScreenRotation.value) {
                        ROTATION_90 -> false
                        ROTATION_270 -> false
                        else -> true
                    }
                    ZoomControlBar(
                        ..., vertical = watermarkVertical,
                        modifier = Modifier.align(watermarkAlignment)
                    )
                }

                // 画面调整浮层 — 水印模式用 RotatedCornerContent 旋转,扫码模式不旋转
                val screenRotation = rememberScreenRotation()
                val rotationAngle = getRotationAngle(screenRotation.value)
                if (selectedTab == "watermark") {
                    // 映射到用户视角左上角:
                    //   ROTATION_0 → TopStart, ROTATION_90 → TopEnd,
                    //   ROTATION_180 → BottomEnd, ROTATION_270 → BottomStart
                    val overlayAlignment = when (screenRotation.value) {
                        ROTATION_0 -> Alignment.TopStart
                        ROTATION_90 -> Alignment.TopEnd
                        ROTATION_180 -> Alignment.BottomEnd
                        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),
                        )
                    }
                } else {
                    // 扫码模式:不旋转,固定左上角
                    Box(modifier = Modifier.fillMaxWidth().align(Alignment.TopCenter)) {
                        CameraAdjustmentOverlay(
                            ...,
                            modifier = Modifier.align(Alignment.TopStart)
                                .padding(top = 8.dp, start = 8.dp),
                        )
                        ScanModeToggle(
                            ..., modifier = Modifier.align(Alignment.TopEnd)
                                .padding(top = 16.dp, end = 16.dp),
                        )
                    }
                }
            }
        },
        bottomBarContent = { /* TabBar + BottomBar */ },
    )
}

5. 文件清单

5.1 基础设施(不改)

文件说明
camera/CameraContainer.ktCameraX 封装(~300 行)。依赖:CameraPermissionManager/CameraHardwareDetector/CameraFocusHandler/CameraControlsApplier
camera/CameraContainerState.kt底座状态 data class(含光学倍数列表等 19 个字段)
camera/CameraPermissionManager.kt权限请求与设置引导弹窗
camera/CameraHardwareDetector.kt光学预设检测/数码变焦范围/微距选择
camera/CameraFocusHandler.kt触控对焦与 🔒 锁定指示圈
camera/CameraControlsApplier.kt曝光补偿/白平衡/色彩效果(硬件优先→软件降级)

5.2 入口(改)

文件说明
CameraScreen.ktTab 委派入口,支持入口参数 + 持久化 + 缩放控制 + CameraAdjustmentOverlay + Snackbar 错误展示
CameraViewModel.kt相机全局状态(仅记录保存)
camera-ui/CameraTopBar.kt纯 UI:闪光/翻转/重定位/设置按钮
camera-ui/CameraTabBar.kt纯 UI:Tab 切换栏
memory/CameraMemoryState.kt相机记忆状态数据模型(22 字段,Gson 序列化)
memory/CameraMemorySchema.ktSchema 数据模型(默认值+可选值+说明)
memory/CameraMemoryRepository.kt双 JSON 文件持久化仓库(StateFlow 暴露)

5.3 旋转基础设施

文件说明
rotation/RotationModels.ktHorizontalAlignment/VerticalAlignment/RotationStrategy/PositionMapping/RotationBlockConfig 数据模型
rotation/RotationUtils.ktgetPositionMapping 位置映射 + RotationSizes 尺寸常量 + getRotationAngle/isLandscape 等工具
rotation/RotationBlock.ktRotationBlock 组合函数(SYNC_ROTATE/FIXED_POSITION/REVERSE_COMPENSATE 三种策略)
rotation/RotatedCornerContent.kt新增:旋转后精确角落定位组件。用自定义 Layout 在测量阶段计算旋转后视觉尺寸,解决 Box(contentAlignment)+Modifier.rotate() 的矩形内容偏移问题。信息水印和 CameraAdjustmentOverlay(水印模式)使用
rotation/CameraRotationContainer.kt统一坐标系容器,始终竖屏 padding(top+bottom),frozen 参数控制扫码模式冻结旋转
rotation/ScreenRotationDetector.ktOrientationEventListener 封装 + rememberScreenRotation()

5.4 子模式

模式文件说明
缩放zoom/ZoomControlBar.kt缩放控件(横向/纵向,水印/扫码通用)。水印模式从 WatermarkModeScreen.kt 移到 CameraScreen.kt 统一管理,通过 vertical 参数 + 动态 Alignment 映射实现贴右边界,不用 Modifier.rotate
扫码scan/ScanModeScreen.kt扫码 UI Shell + 分析器创建(含防抖/扫描线/多码/批量/反馈)
内含 BatchScanFloatingPanel:操作按钮行+"已扫描 N/50"+列表(编号+类型+内容+跳转按钮)
scan/ScanDebouncer.kt3 帧内容一致性防抖 + 批量扫码去重(纯逻辑类,无 Android 依赖)
scan/ScanHistoryScreen.kt扫码历史记录页(分页/分组/搜索/清空)
scan/ScanSettingsScreen.kt扫码专用设置页
相机设置CameraSettingsScreen.kt完整相机设置页(4 段)
CameraSettingsViewModel.kt设置逻辑(从 CameraMemoryRepository 读写)
水印watermark/WatermarkModeScreen.kt水印 UI Shell(含连拍/定时按钮、微距切换按钮)
注意:缩放条和 CameraAdjustmentOverlay 不在本组件内,由 CameraScreen 统一管理
watermark/WatermarkViewModel.kt全流程处理(含连拍/定时/预设)
WatermarkOverlay.kt水印绘制(feature/camera/ 根目录)
WatermarkPreviewOverlay.kt实时预览(feature/camera/ 根目录)。信息水印使用 RotatedCornerContent 旋转到用户视角左下角
WatermarkSettingsSheet.kt设置弹窗(含预设模板,feature/camera/ 根目录)
画面调整cameraui/CameraAdjustmentOverlay.kt画面调整浮层(EV/WB/CLR/FLSH/重置)。水印模式由 CameraScreenRotatedCornerContent 包裹旋转,扫码模式 Box(align=TopStart) 不旋转
(未来)xxx/XxxModeScreen.kt新建子目录
xxx/XxxViewModel.ktViewModel

6. 交叉引用

相关文档内容
/reference/android.md 3.4.1 节相机中心功能描述
/designs/camera/watermark-flow.md水印全流程说明
/architecture/overview.md功能清单概览
/standards/android-coding.md 第4章跨页面数据传递规范(FormStateManager)
/standards/android-coding.md 第7章小部件开发规范
/standards/camera-rotation.md旋转处理规范(含 ZoomControlBar 第 13 节、RotatedCornerContent 第 14 节、CameraAdjustmentOverlay 第 15 节)