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 ebd7844..6ae3ffc 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 @@ -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 + } + } +}