From d8fe374a163aa52be86416fe850c71d542978cb9 Mon Sep 17 00:00:00 2001 From: xiaji Date: Sat, 28 Feb 2026 00:44:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=9C=80=E6=B1=82=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=9A=E4=BD=BF=E7=94=A8=E7=94=A8=E6=88=B7=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=9A=84=E6=B0=B4=E5=8D=B0=E6=A0=B7=E5=BC=8F=E5=92=8C?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E8=B4=A8=E9=87=8F=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=A0=87=E9=A2=98/=E5=86=85=E5=AE=B9=E6=96=87=E5=AD=97?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 70 +++++++++++++++++++ .../camera/ui/camera/CameraScreen.kt | 9 ++- .../inspection/camera/ui/merge/MergeScreen.kt | 31 ++++++-- .../camera/ui/settings/SettingsScreen.kt | 50 +++++++++++++ .../inspection/camera/util/ImageProcessor.kt | 6 +- 5 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..38d1ddf --- /dev/null +++ b/README.md @@ -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 diff --git a/app/src/main/java/com/inspection/camera/ui/camera/CameraScreen.kt b/app/src/main/java/com/inspection/camera/ui/camera/CameraScreen.kt index ee9e8c5..318becd 100644 --- a/app/src/main/java/com/inspection/camera/ui/camera/CameraScreen.kt +++ b/app/src/main/java/com/inspection/camera/ui/camera/CameraScreen.kt @@ -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() } @@ -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() diff --git a/app/src/main/java/com/inspection/camera/ui/merge/MergeScreen.kt b/app/src/main/java/com/inspection/camera/ui/merge/MergeScreen.kt index 3d0dd42..93562a8 100644 --- a/app/src/main/java/com/inspection/camera/ui/merge/MergeScreen.kt +++ b/app/src/main/java/com/inspection/camera/ui/merge/MergeScreen.kt @@ -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().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(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 diff --git a/app/src/main/java/com/inspection/camera/ui/settings/SettingsScreen.kt b/app/src/main/java/com/inspection/camera/ui/settings/SettingsScreen.kt index 1f101d9..3e4a1f0 100644 --- a/app/src/main/java/com/inspection/camera/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/inspection/camera/ui/settings/SettingsScreen.kt @@ -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)) diff --git a/app/src/main/java/com/inspection/camera/util/ImageProcessor.kt b/app/src/main/java/com/inspection/camera/util/ImageProcessor.kt index eadf687..c26d431 100644 --- a/app/src/main/java/com/inspection/camera/util/ImageProcessor.kt +++ b/app/src/main/java/com/inspection/camera/util/ImageProcessor.kt @@ -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,