完善需求功能:使用用户配置的水印样式和图片质量,添加标题/内容文字样式配置

This commit is contained in:
2026-02-28 00:44:52 +08:00
parent aadfd5a296
commit d8fe374a16
5 changed files with 157 additions and 9 deletions

70
README.md Normal file
View File

@@ -0,0 +1,70 @@
# 巡检相机 (Inspection Camera)
一款基于 Android 的巡检拍照应用,支持水印、多图合成、文字编辑等功能。
## 功能特性
### 1. 相机核心模块
- 使用 Android CameraX 库
- 支持拍照、自动/手动对焦、曝光调节
- 闪光灯控制(自动/开/关)
### 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

View File

@@ -62,6 +62,7 @@ import androidx.lifecycle.LifecycleOwner
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.inspection.camera.data.PreferencesManager import com.inspection.camera.data.PreferencesManager
import com.inspection.camera.data.models.ImageQuality
import com.inspection.camera.data.models.WatermarkStyle import com.inspection.camera.data.models.WatermarkStyle
import com.inspection.camera.util.ImageProcessor import com.inspection.camera.util.ImageProcessor
import com.inspection.camera.util.LocationHelper import com.inspection.camera.util.LocationHelper
@@ -90,6 +91,7 @@ fun CameraScreen(
var locationText by remember { mutableStateOf("") } var locationText by remember { mutableStateOf("") }
var manualAddress by remember { mutableStateOf("") } var manualAddress by remember { mutableStateOf("") }
var currentWatermarkStyle by remember { mutableStateOf(WatermarkStyle.Default) } var currentWatermarkStyle by remember { mutableStateOf(WatermarkStyle.Default) }
var currentImageQuality by remember { mutableStateOf(ImageQuality.Standard) }
var showPermissionDeniedDialog by remember { mutableStateOf(false) } var showPermissionDeniedDialog by remember { mutableStateOf(false) }
val capturedImages = remember { mutableStateListOf<Uri>() } val capturedImages = remember { mutableStateListOf<Uri>() }
@@ -120,6 +122,9 @@ fun CameraScreen(
preferencesManager.manualAddress.collect { address -> preferencesManager.manualAddress.collect { address ->
manualAddress = address manualAddress = address
} }
preferencesManager.imageQuality.collect { quality ->
currentImageQuality = quality
}
} }
// 获取位置 // 获取位置
@@ -148,6 +153,7 @@ fun CameraScreen(
context = context, context = context,
flashMode = flashMode, flashMode = flashMode,
watermarkStyle = currentWatermarkStyle, watermarkStyle = currentWatermarkStyle,
imageQuality = currentImageQuality,
locationText = if (locationText.isNotBlank()) locationText else "未知地点", locationText = if (locationText.isNotBlank()) locationText else "未知地点",
onComplete = { uri -> onComplete = { uri ->
capturedImages.add(uri) capturedImages.add(uri)
@@ -398,6 +404,7 @@ private fun capturePhoto(
context: Context, context: Context,
flashMode: Int, flashMode: Int,
watermarkStyle: WatermarkStyle, watermarkStyle: WatermarkStyle,
imageQuality: ImageQuality,
locationText: String, locationText: String,
onComplete: (Uri) -> Unit onComplete: (Uri) -> Unit
) { ) {
@@ -431,7 +438,7 @@ private fun capturePhoto(
// 保存到相册 // 保存到相册
val fileName = ImageProcessor.generateFileName("") val fileName = ImageProcessor.generateFileName("")
val uri = ImageProcessor.saveToGallery(context, watermarkedBitmap, fileName) val uri = ImageProcessor.saveToGallery(context, watermarkedBitmap, fileName, imageQuality.quality)
bitmap.recycle() bitmap.recycle()
watermarkedBitmap.recycle() watermarkedBitmap.recycle()

View File

@@ -56,7 +56,9 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest import coil.request.ImageRequest
import com.inspection.camera.data.PreferencesManager import com.inspection.camera.data.PreferencesManager
import com.inspection.camera.data.models.ImageQuality
import com.inspection.camera.data.models.MergeLayoutType import com.inspection.camera.data.models.MergeLayoutType
import com.inspection.camera.data.models.WatermarkStyle
import com.inspection.camera.ui.theme.Primary import com.inspection.camera.ui.theme.Primary
import com.inspection.camera.util.ImageProcessor import com.inspection.camera.util.ImageProcessor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -76,12 +78,29 @@ fun MergeScreen(
val images = remember { mutableStateListOf<Uri>().apply { addAll(imageUris) } } val images = remember { mutableStateListOf<Uri>().apply { addAll(imageUris) } }
var layoutType by remember { mutableStateOf(MergeLayoutType.Grid2x2) } var layoutType by remember { mutableStateOf(MergeLayoutType.Grid2x2) }
var imageQuality by remember { mutableStateOf(ImageQuality.Standard) }
var titleStyle by remember { mutableStateOf(WatermarkStyle.Default) }
var contentStyle by remember { mutableStateOf(WatermarkStyle.Default) }
var showPreview by remember { mutableStateOf(false) } var showPreview by remember { mutableStateOf(false) }
var previewBitmap by remember { mutableStateOf<Bitmap?>(null) } var previewBitmap by remember { mutableStateOf<Bitmap?>(null) }
var title by remember { mutableStateOf("") } var title by remember { mutableStateOf("") }
var content by remember { mutableStateOf("") } var content by remember { mutableStateOf("") }
var showSaveDialog by remember { mutableStateOf(false) } var showSaveDialog by remember { mutableStateOf(false) }
// 加载用户配置
LaunchedEffect(Unit) {
preferencesManager.mergeLayout.collect { layoutType = it }
}
LaunchedEffect(Unit) {
preferencesManager.imageQuality.collect { imageQuality = it }
}
LaunchedEffect(Unit) {
preferencesManager.titleStyle.collect { titleStyle = it }
}
LaunchedEffect(Unit) {
preferencesManager.contentStyle.collect { contentStyle = it }
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -206,15 +225,15 @@ fun MergeScreen(
ImageProcessor.mergeImages( ImageProcessor.mergeImages(
images.toList(), images.toList(),
layoutType, layoutType,
com.inspection.camera.data.models.ImageQuality.Standard imageQuality
).let { bitmap -> ).let { bitmap ->
if (title.isNotBlank() || content.isNotBlank()) { if (title.isNotBlank() || content.isNotBlank()) {
ImageProcessor.addTextToBitmap( ImageProcessor.addTextToBitmap(
bitmap, bitmap,
title, title,
content, content,
com.inspection.camera.data.models.WatermarkStyle.Default, titleStyle,
com.inspection.camera.data.models.WatermarkStyle.Default contentStyle
) )
} else { } else {
bitmap bitmap
@@ -273,15 +292,15 @@ fun MergeScreen(
ImageProcessor.mergeImages( ImageProcessor.mergeImages(
images.toList(), images.toList(),
layoutType, layoutType,
com.inspection.camera.data.models.ImageQuality.Standard imageQuality
).let { mergedBitmap -> ).let { mergedBitmap ->
if (title.isNotBlank() || content.isNotBlank()) { if (title.isNotBlank() || content.isNotBlank()) {
ImageProcessor.addTextToBitmap( ImageProcessor.addTextToBitmap(
mergedBitmap, mergedBitmap,
title, title,
content, content,
com.inspection.camera.data.models.WatermarkStyle.Default, titleStyle,
com.inspection.camera.data.models.WatermarkStyle.Default contentStyle
) )
} else { } else {
mergedBitmap mergedBitmap

View File

@@ -56,6 +56,8 @@ fun SettingsScreen(
var locationMode by remember { mutableStateOf(LocationMode.Network) } var locationMode by remember { mutableStateOf(LocationMode.Network) }
var mergeLayout by remember { mutableStateOf(MergeLayoutType.Grid2x2) } var mergeLayout by remember { mutableStateOf(MergeLayoutType.Grid2x2) }
var imageQuality by remember { mutableStateOf(ImageQuality.Standard) } var imageQuality by remember { mutableStateOf(ImageQuality.Standard) }
var titleStyle by remember { mutableStateOf(WatermarkStyle.Default) }
var contentStyle by remember { mutableStateOf(WatermarkStyle.Default) }
var defaultTheme by remember { mutableStateOf("") } var defaultTheme by remember { mutableStateOf("") }
var inspectorName by remember { mutableStateOf("") } var inspectorName by remember { mutableStateOf("") }
var manualAddress by remember { mutableStateOf("") } var manualAddress by remember { mutableStateOf("") }
@@ -73,6 +75,12 @@ fun SettingsScreen(
scope.launch { scope.launch {
preferencesManager.imageQuality.collect { imageQuality = it } preferencesManager.imageQuality.collect { imageQuality = it }
} }
scope.launch {
preferencesManager.titleStyle.collect { titleStyle = it }
}
scope.launch {
preferencesManager.contentStyle.collect { contentStyle = it }
}
scope.launch { scope.launch {
preferencesManager.defaultTheme.collect { defaultTheme = it } preferencesManager.defaultTheme.collect { defaultTheme = it }
} }
@@ -210,6 +218,48 @@ fun SettingsScreen(
} }
} }
} }
HorizontalDivider()
SettingsItem(title = "默认标题样式") {
WatermarkStyle.entries.forEach { style ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { scope.launch { preferencesManager.setTitleStyle(style) } }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = titleStyle.name == style.name,
onClick = { scope.launch { preferencesManager.setTitleStyle(style) } }
)
Spacer(modifier = Modifier.width(8.dp))
Text(style.name)
}
}
}
HorizontalDivider()
SettingsItem(title = "默认内容样式") {
WatermarkStyle.entries.forEach { style ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { scope.launch { preferencesManager.setContentStyle(style) } }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = contentStyle.name == style.name,
onClick = { scope.launch { preferencesManager.setContentStyle(style) } }
)
Spacer(modifier = Modifier.width(8.dp))
Text(style.name)
}
}
}
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))

View File

@@ -79,9 +79,11 @@ object ImageProcessor {
val y = result.height - padding val y = result.height - padding
// 绘制背景 // 绘制背景
if (style.backgroundColor != android.graphics.Color.TRANSPARENT) { // 通过ARGB判断来决定是否绘制背景避免混用 Android Color 与 Compose Color 的类型问题
val bgColorInt = style.backgroundColor.toArgb()
if (bgColorInt != 0) {
val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = style.backgroundColor.toArgb() color = bgColorInt
} }
val bgRect = RectF( val bgRect = RectF(
x - 10, x - 10,