外观
相机功能规范
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 新建步骤
- 在
feature/camera/下新建子目录xxx/ - 创建
XxxModeScreen.kt(组合 CameraContainer + 自定义 UI) - 创建
XxxViewModel.kt(业务后处理逻辑) - 在
CameraScreen.kt的 Tab 列表加 1 项 - 不修改 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(...)
}
}固定栏避让配置:
| 模式 | frozen | topBarHeight | bottomBarHeight | 说明 |
|---|---|---|---|---|
| 水印 | false | 0.dp | 20.dp | CameraTopBar 在 PageTemplate.titleBar 槽位,不在 content 内 |
| 扫码 | true | 56.dp | 80.dp | CameraTopBar 在 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.kt | CameraX 封装(~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.kt | Tab 委派入口,支持入口参数 + 持久化 + 缩放控制 + CameraAdjustmentOverlay + Snackbar 错误展示 |
CameraViewModel.kt | 相机全局状态(仅记录保存) |
camera-ui/CameraTopBar.kt | 纯 UI:闪光/翻转/重定位/设置按钮 |
camera-ui/CameraTabBar.kt | 纯 UI:Tab 切换栏 |
memory/CameraMemoryState.kt | 相机记忆状态数据模型(22 字段,Gson 序列化) |
memory/CameraMemorySchema.kt | Schema 数据模型(默认值+可选值+说明) |
memory/CameraMemoryRepository.kt | 双 JSON 文件持久化仓库(StateFlow 暴露) |
5.3 旋转基础设施
| 文件 | 说明 |
|---|---|
rotation/RotationModels.kt | HorizontalAlignment/VerticalAlignment/RotationStrategy/PositionMapping/RotationBlockConfig 数据模型 |
rotation/RotationUtils.kt | getPositionMapping 位置映射 + RotationSizes 尺寸常量 + getRotationAngle/isLandscape 等工具 |
rotation/RotationBlock.kt | RotationBlock 组合函数(SYNC_ROTATE/FIXED_POSITION/REVERSE_COMPENSATE 三种策略) |
rotation/RotatedCornerContent.kt | 新增:旋转后精确角落定位组件。用自定义 Layout 在测量阶段计算旋转后视觉尺寸,解决 Box(contentAlignment)+Modifier.rotate() 的矩形内容偏移问题。信息水印和 CameraAdjustmentOverlay(水印模式)使用 |
rotation/CameraRotationContainer.kt | 统一坐标系容器,始终竖屏 padding(top+bottom),frozen 参数控制扫码模式冻结旋转 |
rotation/ScreenRotationDetector.kt | OrientationEventListener 封装 + rememberScreenRotation() |
5.4 子模式
| 模式 | 文件 | 说明 |
|---|---|---|
| 缩放 | zoom/ZoomControlBar.kt | 缩放控件(横向/纵向,水印/扫码通用)。水印模式从 WatermarkModeScreen.kt 移到 CameraScreen.kt 统一管理,通过 vertical 参数 + 动态 Alignment 映射实现贴右边界,不用 Modifier.rotate |
| 扫码 | scan/ScanModeScreen.kt | 扫码 UI Shell + 分析器创建(含防抖/扫描线/多码/批量/反馈) 内含 BatchScanFloatingPanel:操作按钮行+"已扫描 N/50"+列表(编号+类型+内容+跳转按钮) |
scan/ScanDebouncer.kt | 3 帧内容一致性防抖 + 批量扫码去重(纯逻辑类,无 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/重置)。水印模式由 CameraScreen 用 RotatedCornerContent 包裹旋转,扫码模式 Box(align=TopStart) 不旋转 |
| (未来) | xxx/XxxModeScreen.kt | 新建子目录 |
xxx/XxxViewModel.kt | ViewModel |
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 节) |