Compare commits
2 Commits
b20d5356d5
...
1aab8b2d45
| Author | SHA1 | Date | |
|---|---|---|---|
| 1aab8b2d45 | |||
| 3d46c93786 |
78
README.md
78
README.md
@@ -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
|
||||
- AirTest:airtest 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 或文档。
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user