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. 水印处理模块
|
如需更多安装与运行指南,请查看 respective CI/CD 或文档。
|
||||||
- 拍摄后自动在照片左下角叠加时间与地点水印
|
|
||||||
- 时间格式: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
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.CameraAlt
|
import androidx.compose.material.icons.filled.CameraAlt
|
||||||
import androidx.compose.material.icons.filled.PhotoLibrary
|
import androidx.compose.material.icons.filled.PhotoLibrary
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.GridView
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
@@ -22,6 +23,8 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
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.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
@@ -54,8 +57,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
sealed class Screen(val route: String, val title: String, val icon: ImageVector) {
|
sealed class Screen(val route: String, val title: String, val icon: ImageVector) {
|
||||||
data object Camera : Screen("camera", "相机", Icons.Default.CameraAlt)
|
data object Camera : Screen("camera", "相机", Icons.Default.CameraAlt)
|
||||||
data object Gallery : Screen("gallery", "相册", Icons.Default.PhotoLibrary)
|
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 Settings : Screen("settings", "设置", Icons.Default.Settings)
|
||||||
data object Merge : Screen("merge", "合成", Icons.Default.CameraAlt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -69,11 +72,11 @@ fun MainApp(preferencesManager: PreferencesManager) {
|
|||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentRoute = navBackStackEntry?.destination?.route
|
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 {
|
NavigationBar {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
icon = { Icon(Icons.Default.CameraAlt, contentDescription = "相机") },
|
icon = { Icon(Icons.Default.CameraAlt, contentDescription = "相机") },
|
||||||
label = { Text("相机") },
|
label = { Text(stringResource(R.string.camera)) },
|
||||||
selected = currentRoute == Screen.Camera.route,
|
selected = currentRoute == Screen.Camera.route,
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate(Screen.Camera.route) {
|
navController.navigate(Screen.Camera.route) {
|
||||||
@@ -83,7 +86,7 @@ fun MainApp(preferencesManager: PreferencesManager) {
|
|||||||
)
|
)
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
icon = { Icon(Icons.Default.PhotoLibrary, contentDescription = "相册") },
|
icon = { Icon(Icons.Default.PhotoLibrary, contentDescription = "相册") },
|
||||||
label = { Text("相册") },
|
label = { Text(stringResource(R.string.gallery)) },
|
||||||
selected = currentRoute == Screen.Gallery.route,
|
selected = currentRoute == Screen.Gallery.route,
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate(Screen.Gallery.route) {
|
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(
|
NavigationBarItem(
|
||||||
icon = { Icon(Icons.Default.Settings, contentDescription = "设置") },
|
icon = { Icon(Icons.Default.Settings, contentDescription = "设置") },
|
||||||
label = { Text("设置") },
|
label = { Text(stringResource(R.string.settings)) },
|
||||||
selected = currentRoute == Screen.Settings.route,
|
selected = currentRoute == Screen.Settings.route,
|
||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate(Screen.Settings.route) {
|
navController.navigate(Screen.Settings.route) {
|
||||||
|
|||||||
@@ -308,15 +308,7 @@ private fun CameraContent(
|
|||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
|
|
||||||
// 顶部栏
|
// 底部控制栏 - 只保留拍照按钮(其他功能在底部导航栏)
|
||||||
TopControls(
|
|
||||||
flashMode = flashMode,
|
|
||||||
onFlashModeChange = onFlashModeChange,
|
|
||||||
onSettingsClick = onSettingsClick,
|
|
||||||
modifier = Modifier.align(Alignment.TopCenter)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 底部控制栏
|
|
||||||
BottomControls(
|
BottomControls(
|
||||||
capturedCount = capturedCount,
|
capturedCount = capturedCount,
|
||||||
imageCapture = imageCapture,
|
imageCapture = imageCapture,
|
||||||
@@ -383,22 +375,7 @@ private fun BottomControls(
|
|||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
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(
|
FloatingActionButton(
|
||||||
onClick = { imageCapture?.let { onCapture(it) } },
|
onClick = { imageCapture?.let { onCapture(it) } },
|
||||||
modifier = Modifier.size(72.dp),
|
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="exposure">曝光</string>
|
||||||
<string name="watermark">水印</string>
|
<string name="watermark">水印</string>
|
||||||
<string name="merge">合成</string>
|
<string name="merge">合成</string>
|
||||||
|
<string name="puzzle">拼图</string>
|
||||||
<string name="edit">编辑</string>
|
<string name="edit">编辑</string>
|
||||||
<string name="save">保存</string>
|
<string name="save">保存</string>
|
||||||
<string name="share">分享</string>
|
<string name="share">分享</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user