Compare commits

...

2 Commits

4 changed files with 47 additions and 129 deletions

View File

@@ -1,70 +1,16 @@
# 巡检相机 (Inspection Camera)
# CheckShot
一款基于 Android 的巡检拍照应用,支持水印、多图合成、文字编辑等功能
Android 图片检查与拼图工具。实现了水印、拼图、设置等核心功能,配合 AirTest 自动化测试
## 功能特性
核心特性
- 水印模块:时间水印、地点水印、三种预设样式。
- 拼图模块:多图合成,固定模板,图片替换/删除,合成质量控制。
- 设置:默认合成布局、默认水印样式、默认主题、巡检员信息等。
- 测试AirTest 集成用例,覆盖水印、相册、拼图、设置等场景。
### 1. 相机核心模块
- 使用 Android CameraX 库
- 支持拍照、自动/手动对焦、曝光调节
- 闪光灯控制(自动/开/关)
快速开始
- 构建:./gradlew assembleDebug
- 安装adb install -r app/build/outputs/apk/debug/app-debug.apk
- AirTestairtest run test/airtest/...
### 2. 水印处理模块
- 拍摄后自动在照片左下角叠加时间与地点水印
- 时间格式yyyy年-MM月-dd日 HH:mm:ss
- 地点水印:支持 Geocoder 联网解析地址或降级显示经纬度
- 多种预设水印样式可选(默认样式、简约样式、醒目样式)
### 3. 多图合成模块
- 支持图片拼接2x2网格、1+3布局、3+1布局
- 基于模板的布局编辑(替换/删除图片)
- 合成质量控制(高清/标准/流畅)
### 4. 文字编辑模块
- 支持在合成图片的顶部(标题)和底部(内容)添加文字
- 智能换行
- 多种预设文字样式可选
### 5. 图片管理模块
- 本地存储、分类管理、预览
- 导出/分享功能
- 严格遵循分区存储规则,通过 MediaStore 保存到系统相册
### 6. 权限管理
- 相机权限
- 定位权限
- 支持手动输入地址作为降级方案
## 配置设置
### 水印设置
- 水印样式选择
- 地点获取方式(联网查询/经纬度)
### 合成与文字设置
- 默认合成布局
- 合成图片质量
- 默认标题样式
- 默认内容样式
### 通用设置
- 默认巡检主题
- 巡检员信息
## 技术栈
- **语言**: Kotlin
- **UI框架**: Jetpack Compose
- **相机**: CameraX
- **存储**: DataStore Preferences
- **定位**: Google Play Services Location
## 权限说明
- `CAMERA`: 相机拍照
- `ACCESS_FINE_LOCATION`: 精确定位
- `ACCESS_COARSE_LOCATION`: 粗略定位
## 版本
当前版本1.0.0
如需更多安装与运行指南,请查看 respective CI/CD 或文档。

View File

@@ -10,6 +10,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.PhotoLibrary
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
@@ -22,6 +23,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import com.inspection.camera.R
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@@ -54,8 +57,8 @@ class MainActivity : ComponentActivity() {
sealed class Screen(val route: String, val title: String, val icon: ImageVector) {
data object Camera : Screen("camera", "相机", Icons.Default.CameraAlt)
data object Gallery : Screen("gallery", "相册", Icons.Default.PhotoLibrary)
data object Merge : Screen("merge", "拼图", Icons.Default.GridView)
data object Settings : Screen("settings", "设置", Icons.Default.Settings)
data object Merge : Screen("merge", "合成", Icons.Default.CameraAlt)
}
@Composable
@@ -69,11 +72,11 @@ fun MainApp(preferencesManager: PreferencesManager) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
if (currentRoute in listOf(Screen.Camera.route, Screen.Gallery.route, Screen.Settings.route)) {
if (currentRoute in listOf(Screen.Camera.route, Screen.Gallery.route, Screen.Merge.route, Screen.Settings.route)) {
NavigationBar {
NavigationBarItem(
icon = { Icon(Icons.Default.CameraAlt, contentDescription = "相机") },
label = { Text("相机") },
label = { Text(stringResource(R.string.camera)) },
selected = currentRoute == Screen.Camera.route,
onClick = {
navController.navigate(Screen.Camera.route) {
@@ -83,7 +86,7 @@ fun MainApp(preferencesManager: PreferencesManager) {
)
NavigationBarItem(
icon = { Icon(Icons.Default.PhotoLibrary, contentDescription = "相册") },
label = { Text("相册") },
label = { Text(stringResource(R.string.gallery)) },
selected = currentRoute == Screen.Gallery.route,
onClick = {
navController.navigate(Screen.Gallery.route) {
@@ -91,9 +94,19 @@ fun MainApp(preferencesManager: PreferencesManager) {
}
}
)
NavigationBarItem(
icon = { Icon(Icons.Default.GridView, contentDescription = "拼图") },
label = { Text(stringResource(R.string.puzzle)) },
selected = currentRoute == Screen.Merge.route,
onClick = {
navController.navigate(Screen.Merge.route) {
popUpTo(Screen.Camera.route)
}
}
)
NavigationBarItem(
icon = { Icon(Icons.Default.Settings, contentDescription = "设置") },
label = { Text("设置") },
label = { Text(stringResource(R.string.settings)) },
selected = currentRoute == Screen.Settings.route,
onClick = {
navController.navigate(Screen.Settings.route) {

View File

@@ -308,15 +308,7 @@ private fun CameraContent(
modifier = Modifier.fillMaxSize()
)
// 顶部栏
TopControls(
flashMode = flashMode,
onFlashModeChange = onFlashModeChange,
onSettingsClick = onSettingsClick,
modifier = Modifier.align(Alignment.TopCenter)
)
// 底部控制栏
// 底部控制栏 - 只保留拍照按钮(其他功能在底部导航栏)
BottomControls(
capturedCount = capturedCount,
imageCapture = imageCapture,
@@ -383,22 +375,7 @@ private fun BottomControls(
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onGalleryClick) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = Icons.Default.LocationOn,
contentDescription = "相册",
tint = Color.White
)
Text(text = "相册", style = MaterialTheme.typography.labelSmall, color = Color.White)
}
}
// 只保留中间的拍照按钮
FloatingActionButton(
onClick = { imageCapture?.let { onCapture(it) } },
modifier = Modifier.size(72.dp),
@@ -416,25 +393,6 @@ private fun BottomControls(
)
}
}
IconButton(
onClick = onMergeClick,
enabled = capturedCount > 0
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "合成",
tint = if (capturedCount > 0) Color.White else Color.Gray
)
Text(
text = "合成($capturedCount)",
style = MaterialTheme.typography.labelSmall,
color = if (capturedCount > 0) Color.White else Color.Gray
)
}
}
}
}
}

View File

@@ -13,6 +13,7 @@
<string name="exposure">曝光</string>
<string name="watermark">水印</string>
<string name="merge">合成</string>
<string name="puzzle">拼图</string>
<string name="edit">编辑</string>
<string name="save">保存</string>
<string name="share">分享</string>