feat(puzzle merge): fix URI handling by converting to local path; ensure preview includes text overlays and layout changes
This commit is contained in:
@@ -2,6 +2,7 @@ package com.inspection.camera.ui.merge
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
// java.io.File and FileOutputStream will be referenced with fully qualified names to avoid ambiguity
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -22,10 +23,14 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -66,6 +71,8 @@ import com.inspection.camera.util.ImageProcessor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -88,6 +95,23 @@ fun MergeScreen(
|
||||
var title by remember { mutableStateOf("") }
|
||||
var content by remember { mutableStateOf("") }
|
||||
var showSaveDialog by remember { mutableStateOf(false) }
|
||||
var selectedImageIndex by remember { mutableStateOf(-1) }
|
||||
|
||||
// 图片选择器
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedImageIndex = -1
|
||||
}
|
||||
|
||||
// 加载用户配置
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -129,18 +153,51 @@ fun MergeScreen(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MergeLayoutType.entries.forEach { layout ->
|
||||
LayoutOption(
|
||||
layout = layout,
|
||||
isSelected = layoutType == layout,
|
||||
onClick = { layoutType = layout }
|
||||
onClick = {
|
||||
layoutType = layout
|
||||
if (images.size > layout.maxImages) {
|
||||
while (images.size > layout.maxImages) {
|
||||
images.removeAt(images.size - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 质量选择
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("质量:", style = MaterialTheme.typography.bodySmall)
|
||||
ImageQuality.entries.forEach { quality ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(if (imageQuality == quality) Primary else Color.LightGray)
|
||||
.clickable { imageQuality = quality }
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = quality.displayName,
|
||||
color = if (imageQuality == quality) Color.White else Color.Black,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图片网格
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(layoutType.cols),
|
||||
@@ -157,6 +214,10 @@ fun MergeScreen(
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.LightGray)
|
||||
.clickable {
|
||||
selectedImageIndex = index
|
||||
imagePickerLauncher.launch("image/*")
|
||||
}
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
@@ -183,6 +244,42 @@ fun MergeScreen(
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// 替换图标
|
||||
Icon(
|
||||
Icons.Default.Refresh,
|
||||
contentDescription = "替换",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(32.dp)
|
||||
.padding(4.dp)
|
||||
.background(Color.Black.copy(alpha = 0.5f), CircleShape)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加图片按钮
|
||||
if (images.size < layoutType.maxImages) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.LightGray.copy(alpha = 0.5f))
|
||||
.clickable {
|
||||
imagePickerLauncher.launch("image/*")
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = "添加图片",
|
||||
tint = Color.Gray,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +321,10 @@ fun MergeScreen(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
previewBitmap = withContext(Dispatchers.Default) {
|
||||
val imageItems = images.map { ImageItem(uri = it, path = it.toString()) }
|
||||
val imageItems = images.map { uri ->
|
||||
val path = convertUriToPath(context, uri)
|
||||
ImageItem(uri = uri, path = path ?: uri.toString())
|
||||
}
|
||||
ImageProcessor.mergeImages(
|
||||
imageItems,
|
||||
layoutType,
|
||||
@@ -340,6 +440,9 @@ private fun LayoutOption(
|
||||
MergeLayoutType.Grid2x2 -> "2x2"
|
||||
MergeLayoutType.Grid1x3 -> "1+3"
|
||||
MergeLayoutType.Grid3x1 -> "3+1"
|
||||
MergeLayoutType.Grid1x2 -> "1+2"
|
||||
MergeLayoutType.Grid2x1 -> "2+1"
|
||||
MergeLayoutType.Grid1x1 -> "单图"
|
||||
}
|
||||
|
||||
Box(
|
||||
@@ -365,3 +468,19 @@ 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