简化拼图布局:只保留2x2和3x3两种网格布局
This commit is contained in:
@@ -8,7 +8,7 @@ CheckShot 是一个面向 Android 的图片处理与检查工具,包含水印
|
|||||||
- 地点水印:优先通过 Geocoder 联网解析地址,失败时回落显示经纬度。可在设置中配置校准方式。
|
- 地点水印:优先通过 Geocoder 联网解析地址,失败时回落显示经纬度。可在设置中配置校准方式。
|
||||||
- 样式:提供三种预设样式(默认/简约/醒目),并可在设置中预览和应用。
|
- 样式:提供三种预设样式(默认/简约/醒目),并可在设置中预览和应用。
|
||||||
- 多图拼图(合成)模块
|
- 多图拼图(合成)模块
|
||||||
- 布局规则:核心布局为 2x2 网格,且支持扩展布局如 1+3、3+1、1+2、2+1、单图等,图片自动缩放裁剪以适配网格。
|
- 布局规则:支持 2x2 和 3x3 两种网格布局,图片自动缩放裁剪以适配网格。
|
||||||
- 核心能力:图片拼接、模板化布局编辑(替换/删除图片)、合成质量控制(分辨率/清晰度)。
|
- 核心能力:图片拼接、模板化布局编辑(替换/删除图片)、合成质量控制(分辨率/清晰度)。
|
||||||
- 交互:支持替换网格中的图片、删除图片、添加新图片、设置合成质量和文本水印文本。
|
- 交互:支持替换网格中的图片、删除图片、添加新图片、设置合成质量和文本水印文本。
|
||||||
- 设置与通用配置
|
- 设置与通用配置
|
||||||
|
|||||||
@@ -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) {
|
enum class MergeLayoutType(val rows: Int, val cols: Int, val displayName: String, val maxImages: Int) {
|
||||||
Grid2x2(2, 2, "2x2网格"),
|
Grid2x2(2, 2, "2x2网格", 4),
|
||||||
Grid1x3(1, 3, "1+3布局"),
|
Grid3x3(3, 3, "3x3网格", 9)
|
||||||
Grid3x1(3, 1, "3+1布局")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ fun MergeScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val images = remember { mutableStateListOf<Uri>().apply { addAll(imageUris) } }
|
data class ImageWithCache(val uri: Uri, val cachePath: String?)
|
||||||
|
|
||||||
|
val images = remember { mutableStateListOf<ImageWithCache>() }
|
||||||
var layoutType by remember { mutableStateOf(MergeLayoutType.Grid2x2) }
|
var layoutType 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 titleStyle by remember { mutableStateOf(WatermarkStyle.Default) }
|
||||||
@@ -97,16 +99,48 @@ fun MergeScreen(
|
|||||||
var showSaveDialog by remember { mutableStateOf(false) }
|
var showSaveDialog by remember { mutableStateOf(false) }
|
||||||
var selectedImageIndex by remember { mutableStateOf(-1) }
|
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(
|
val imagePickerLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.GetMultipleContents()
|
contract = ActivityResultContracts.GetMultipleContents()
|
||||||
) { uris ->
|
) { uris ->
|
||||||
if (uris.isNotEmpty()) {
|
if (uris.isNotEmpty()) {
|
||||||
if (selectedImageIndex >= 0 && selectedImageIndex < images.size) {
|
scope.launch {
|
||||||
images[selectedImageIndex] = uris.first()
|
if (selectedImageIndex >= 0 && selectedImageIndex < images.size) {
|
||||||
} else {
|
// 替换指定位置的图片
|
||||||
if (images.size < layoutType.maxImages) {
|
val path = copyImageToCache(uris.first())
|
||||||
images.addAll(uris.take(layoutType.maxImages - images.size))
|
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),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
itemsIndexed(images) { index, uri ->
|
itemsIndexed(images) { index, imageWithCache ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
@@ -221,7 +255,7 @@ fun MergeScreen(
|
|||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(context)
|
model = ImageRequest.Builder(context)
|
||||||
.data(uri)
|
.data(imageWithCache.uri)
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -321,11 +355,14 @@ fun MergeScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
previewBitmap = withContext(Dispatchers.Default) {
|
previewBitmap = withContext(Dispatchers.Default) {
|
||||||
val imageItems = images.map { uri ->
|
val imageItems = images.map { img ->
|
||||||
val path = convertUriToPath(context, uri)
|
ImageItem(
|
||||||
ImageItem(uri = uri, path = path ?: uri.toString())
|
uri = img.uri,
|
||||||
|
path = img.cachePath ?: img.uri.toString()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ImageProcessor.mergeImages(
|
ImageProcessor.mergeImages(
|
||||||
|
context,
|
||||||
imageItems,
|
imageItems,
|
||||||
layoutType,
|
layoutType,
|
||||||
imageQuality
|
imageQuality
|
||||||
@@ -392,8 +429,14 @@ fun MergeScreen(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val bitmap = withContext(Dispatchers.Default) {
|
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(
|
ImageProcessor.mergeImages(
|
||||||
|
context,
|
||||||
imageItems,
|
imageItems,
|
||||||
layoutType,
|
layoutType,
|
||||||
imageQuality
|
imageQuality
|
||||||
@@ -438,11 +481,7 @@ private fun LayoutOption(
|
|||||||
) {
|
) {
|
||||||
val displayText = when (layout) {
|
val displayText = when (layout) {
|
||||||
MergeLayoutType.Grid2x2 -> "2x2"
|
MergeLayoutType.Grid2x2 -> "2x2"
|
||||||
MergeLayoutType.Grid1x3 -> "1+3"
|
MergeLayoutType.Grid3x3 -> "3x3"
|
||||||
MergeLayoutType.Grid3x1 -> "3+1"
|
|
||||||
MergeLayoutType.Grid1x2 -> "1+2"
|
|
||||||
MergeLayoutType.Grid2x1 -> "2+1"
|
|
||||||
MergeLayoutType.Grid1x1 -> "单图"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user