完善需求功能:使用用户配置的水印样式和图片质量,添加标题/内容文字样式配置
This commit is contained in:
70
README.md
Normal file
70
README.md
Normal 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user