diff --git a/README.md b/README.md index 3c65c1d..04a6771 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ CheckShot 是一个面向 Android 的图片处理与检查工具,包含水印 - 地点水印:优先通过 Geocoder 联网解析地址,失败时回落显示经纬度。可在设置中配置校准方式。 - 样式:提供三种预设样式(默认/简约/醒目),并可在设置中预览和应用。 - 多图拼图(合成)模块 - - 布局规则:核心布局为 2x2 网格,且支持扩展布局如 1+3、3+1、1+2、2+1、单图等,图片自动缩放裁剪以适配网格。 + - 布局规则:支持 2x2 和 3x3 两种网格布局,图片自动缩放裁剪以适配网格。 - 核心能力:图片拼接、模板化布局编辑(替换/删除图片)、合成质量控制(分辨率/清晰度)。 - 交互:支持替换网格中的图片、删除图片、添加新图片、设置合成质量和文本水印文本。 - 设置与通用配置 diff --git a/app/src/main/java/com/inspection/camera/data/models/WatermarkModels.kt b/app/src/main/java/com/inspection/camera/data/models/WatermarkModels.kt index 9816589..ef9d829 100644 --- a/app/src/main/java/com/inspection/camera/data/models/WatermarkModels.kt +++ b/app/src/main/java/com/inspection/camera/data/models/WatermarkModels.kt @@ -60,8 +60,7 @@ enum class ImageQuality(val quality: Int, val displayName: String) { /** * 合成布局类型 */ -enum class MergeLayoutType(val rows: Int, val cols: Int, val displayName: String) { - Grid2x2(2, 2, "2x2网格"), - Grid1x3(1, 3, "1+3布局"), - Grid3x1(3, 1, "3+1布局") +enum class MergeLayoutType(val rows: Int, val cols: Int, val displayName: String, val maxImages: Int) { + Grid2x2(2, 2, "2x2网格", 4), + Grid3x3(3, 3, "3x3网格", 9) } 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 6ae3ffc..29003d2 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 @@ -85,7 +85,9 @@ fun MergeScreen( val context = LocalContext.current val scope = rememberCoroutineScope() - val images = remember { mutableStateListOf().apply { addAll(imageUris) } } + data class ImageWithCache(val uri: Uri, val cachePath: String?) + + val images = remember { mutableStateListOf() } var layoutType by remember { mutableStateOf(MergeLayoutType.Grid2x2) } var imageQuality by remember { mutableStateOf(ImageQuality.Standard) } var titleStyle by remember { mutableStateOf(WatermarkStyle.Default) } @@ -97,16 +99,48 @@ fun MergeScreen( var showSaveDialog by remember { mutableStateOf(false) } var selectedImageIndex by remember { mutableStateOf(-1) } + // 将 URI 图片复制到缓存目录 + suspend fun copyImageToCache(uri: android.net.Uri): String? { + return withContext(Dispatchers.IO) { + try { + val input = context.contentResolver.openInputStream(uri) ?: return@withContext null + val cacheDir = context.cacheDir + val imgDir = java.io.File(cacheDir, "merge_images") + if (!imgDir.exists()) { + imgDir.mkdirs() + } + val fileName = "img_${System.nanoTime()}.jpg" + val outFile = java.io.File(imgDir, fileName) + java.io.FileOutputStream(outFile).use { output -> + input.use { inStream -> inStream.copyTo(output) } + } + outFile.absolutePath + } catch (e: Exception) { + null + } + } + } + // 图片选择器 val imagePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetMultipleContents() ) { uris -> if (uris.isNotEmpty()) { - if (selectedImageIndex >= 0 && selectedImageIndex < images.size) { - images[selectedImageIndex] = uris.first() - } else { - if (images.size < layoutType.maxImages) { - images.addAll(uris.take(layoutType.maxImages - images.size)) + scope.launch { + if (selectedImageIndex >= 0 && selectedImageIndex < images.size) { + // 替换指定位置的图片 + val path = copyImageToCache(uris.first()) + if (path != null) { + images[selectedImageIndex] = ImageWithCache(uris.first(), path) + } + } else { + // 添加新图片 + uris.take(layoutType.maxImages - images.size).forEach { uri -> + val path = copyImageToCache(uri) + if (path != null) { + images.add(ImageWithCache(uri, path)) + } + } } } } @@ -208,7 +242,7 @@ fun MergeScreen( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - itemsIndexed(images) { index, uri -> + itemsIndexed(images) { index, imageWithCache -> Box( modifier = Modifier .aspectRatio(1f) @@ -221,7 +255,7 @@ fun MergeScreen( ) { AsyncImage( model = ImageRequest.Builder(context) - .data(uri) + .data(imageWithCache.uri) .crossfade(true) .build(), contentDescription = null, @@ -321,11 +355,14 @@ fun MergeScreen( onClick = { scope.launch { previewBitmap = withContext(Dispatchers.Default) { - val imageItems = images.map { uri -> - val path = convertUriToPath(context, uri) - ImageItem(uri = uri, path = path ?: uri.toString()) + val imageItems = images.map { img -> + ImageItem( + uri = img.uri, + path = img.cachePath ?: img.uri.toString() + ) } ImageProcessor.mergeImages( + context, imageItems, layoutType, imageQuality @@ -392,8 +429,14 @@ fun MergeScreen( TextButton(onClick = { scope.launch { val bitmap = withContext(Dispatchers.Default) { - val imageItems = images.map { ImageItem(uri = it, path = it.toString()) } + val imageItems = images.map { img -> + ImageItem( + uri = img.uri, + path = img.cachePath ?: img.uri.toString() + ) + } ImageProcessor.mergeImages( + context, imageItems, layoutType, imageQuality @@ -438,11 +481,7 @@ private fun LayoutOption( ) { val displayText = when (layout) { MergeLayoutType.Grid2x2 -> "2x2" - MergeLayoutType.Grid1x3 -> "1+3" - MergeLayoutType.Grid3x1 -> "3+1" - MergeLayoutType.Grid1x2 -> "1+2" - MergeLayoutType.Grid2x1 -> "2+1" - MergeLayoutType.Grid1x1 -> "单图" + MergeLayoutType.Grid3x3 -> "3x3" } Box( @@ -468,19 +507,3 @@ private fun LayoutOption( } } } - -// 将 Content Uri 拷贝到缓存目录,返回本地文件路径(可被 BitmapFactory.decodeFile 使用) -private suspend fun convertUriToPath(context: android.content.Context, uri: android.net.Uri): String? { - return withContext(Dispatchers.IO) { - try { - val input = context.contentResolver.openInputStream(uri) ?: return@withContext null - val tmpFile = java.io.File(context.cacheDir, "img_${System.nanoTime()}.jpg") - java.io.FileOutputStream(tmpFile).use { output -> - input.use { inStream -> inStream.copyTo(output) } - } - tmpFile.absolutePath - } catch (e: Exception) { - null - } - } -}