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

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

View File

@@ -56,7 +56,9 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
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.WatermarkStyle
import com.inspection.camera.ui.theme.Primary
import com.inspection.camera.util.ImageProcessor
import kotlinx.coroutines.Dispatchers
@@ -76,12 +78,29 @@ fun MergeScreen(
val images = remember { mutableStateListOf<Uri>().apply { addAll(imageUris) } }
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 previewBitmap by remember { mutableStateOf<Bitmap?>(null) }
var title by remember { mutableStateOf("") }
var content by remember { mutableStateOf("") }
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(
topBar = {
TopAppBar(
@@ -206,15 +225,15 @@ fun MergeScreen(
ImageProcessor.mergeImages(
images.toList(),
layoutType,
com.inspection.camera.data.models.ImageQuality.Standard
imageQuality
).let { bitmap ->
if (title.isNotBlank() || content.isNotBlank()) {
ImageProcessor.addTextToBitmap(
bitmap,
title,
content,
com.inspection.camera.data.models.WatermarkStyle.Default,
com.inspection.camera.data.models.WatermarkStyle.Default
titleStyle,
contentStyle
)
} else {
bitmap
@@ -273,15 +292,15 @@ fun MergeScreen(
ImageProcessor.mergeImages(
images.toList(),
layoutType,
com.inspection.camera.data.models.ImageQuality.Standard
imageQuality
).let { mergedBitmap ->
if (title.isNotBlank() || content.isNotBlank()) {
ImageProcessor.addTextToBitmap(
mergedBitmap,
title,
content,
com.inspection.camera.data.models.WatermarkStyle.Default,
com.inspection.camera.data.models.WatermarkStyle.Default
titleStyle,
contentStyle
)
} else {
mergedBitmap

View File

@@ -56,6 +56,8 @@ fun SettingsScreen(
var locationMode by remember { mutableStateOf(LocationMode.Network) }
var mergeLayout 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 defaultTheme by remember { mutableStateOf("") }
var inspectorName by remember { mutableStateOf("") }
var manualAddress by remember { mutableStateOf("") }
@@ -73,6 +75,12 @@ fun SettingsScreen(
scope.launch {
preferencesManager.imageQuality.collect { imageQuality = it }
}
scope.launch {
preferencesManager.titleStyle.collect { titleStyle = it }
}
scope.launch {
preferencesManager.contentStyle.collect { contentStyle = it }
}
scope.launch {
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))

View File

@@ -79,9 +79,11 @@ object ImageProcessor {
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 {
color = style.backgroundColor.toArgb()
color = bgColorInt
}
val bgRect = RectF(
x - 10,