From 9455720516288e2976aeaec7947e2e0a9e5d5d38 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 21 Apr 2026 22:33:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=B8=E5=86=8C=E6=8C=89=E6=97=A5?= =?UTF-8?q?=E6=9C=9F=E5=88=86=E7=BB=84=EF=BC=88=E4=BB=8A=E5=A4=A9/?= =?UTF-8?q?=E6=98=A8=E5=A4=A9/=E4=B8=8A=E6=9C=88/=E6=9B=B4=E6=97=A9?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../camera/ui/gallery/GalleryScreen.kt | 248 ++++++++---------- 1 file changed, 116 insertions(+), 132 deletions(-) diff --git a/app/src/main/java/com/inspection/camera/ui/gallery/GalleryScreen.kt b/app/src/main/java/com/inspection/camera/ui/gallery/GalleryScreen.kt index 3058b93..5fe3358 100644 --- a/app/src/main/java/com/inspection/camera/ui/gallery/GalleryScreen.kt +++ b/app/src/main/java/com/inspection/camera/ui/gallery/GalleryScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio @@ -18,13 +17,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Share @@ -61,33 +58,37 @@ import com.inspection.camera.ui.theme.Primary import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.Calendar + +data class GalleryImage(val uri: Uri, val dateAdded: Long) + +data class GallerySection(val title: String, val images: List) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun GalleryScreen( - onNavigateBack: () -> Unit -) { +fun GalleryScreen(onNavigateBack: () -> Unit) { val context = LocalContext.current val scope = rememberCoroutineScope() - var images by remember { mutableStateOf>(emptyList()) } + var images by remember { mutableStateOf>(emptyList()) } var selectedImages by remember { mutableStateOf>(emptySet()) } var showDeleteDialog by remember { mutableStateOf(false) } var isSelectionMode by remember { mutableStateOf(false) } var selectedImageUri by remember { mutableStateOf(null) } - // 加载图片 LaunchedEffect(Unit) { images = loadImagesFromGallery(context) } + val sections: List = remember(images) { + categorizeImages(images) + } + Scaffold( topBar = { TopAppBar( title = { - Text( - text = if (isSelectionMode) "${selectedImages.size} 张已选择" else "相册" - ) + Text(text = if (isSelectionMode) "${selectedImages.size} 张已选择" else "相册") }, navigationIcon = { IconButton(onClick = { @@ -128,49 +129,60 @@ fun GalleryScreen( ) { paddingValues -> if (images.isEmpty()) { Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), + modifier = Modifier.fillMaxSize().padding(paddingValues), contentAlignment = Alignment.Center ) { Text("暂无图片", style = MaterialTheme.typography.bodyLarge) } } else { - LazyVerticalGrid( - columns = GridCells.Fixed(3), - contentPadding = PaddingValues(4.dp), - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) + LazyColumn( + modifier = Modifier.fillMaxSize().padding(paddingValues) ) { - items(images) { uri -> - ImageItem( - uri = uri, - isSelected = selectedImages.contains(uri), - isSelectionMode = isSelectionMode, - onClick = { - if (isSelectionMode) { - selectedImages = if (selectedImages.contains(uri)) { - selectedImages - uri - } else { - selectedImages + uri + sections.forEach { section -> + item { + Text( + text = section.title, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp) + ) + } + val rows = section.images.chunked(3) + items(rows.size) { rowIndex -> + val row = rows[rowIndex] + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + row.forEach { img -> + Box( + modifier = Modifier.weight(1f).aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)) + .background(Color.LightGray) + .clickable { + selectedImageUri = img.uri + } + ) { + AsyncImage( + model = ImageRequest.Builder(context).data(img.uri).crossfade(true).build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) } - } else { - // 非选择模式下,点击打开大图查看器 - selectedImageUri = uri } - }, - onLongClick = { - isSelectionMode = true - selectedImages = selectedImages + uri + repeat(3 - row.size) { + Spacer(modifier = Modifier.weight(1f)) + } } - ) + } + item { + Spacer(modifier = Modifier.height(8.dp)) + } } } } } - // 删除确认对话框 if (showDeleteDialog) { AlertDialog( onDismissRequest = { showDeleteDialog = false }, @@ -199,7 +211,6 @@ fun GalleryScreen( ) } - // 大图查看对话框(支持双指缩放) if (selectedImageUri != null) { var showDeleteConfirm by remember { mutableStateOf(false) } @@ -214,11 +225,7 @@ fun GalleryScreen( Text("图片预览") Row { IconButton(onClick = { showDeleteConfirm = true }) { - Icon( - Icons.Default.Delete, - contentDescription = "删除", - tint = Color.Red - ) + Icon(Icons.Default.Delete, contentDescription = "删除", tint = Color.Red) } IconButton(onClick = { selectedImageUri = null }) { Icon(Icons.Default.Close, contentDescription = "关闭") @@ -229,9 +236,7 @@ fun GalleryScreen( text = { ZoomableImage( imageUri = selectedImageUri!!, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1f) + modifier = Modifier.fillMaxWidth().aspectRatio(1f) ) }, confirmButton = { @@ -241,7 +246,6 @@ fun GalleryScreen( } ) - // 删除二次确认对话框 if (showDeleteConfirm) { AlertDialog( onDismissRequest = { showDeleteConfirm = false }, @@ -270,10 +274,7 @@ fun GalleryScreen( } @Composable -private fun ZoomableImage( - imageUri: Uri, - modifier: Modifier = Modifier -) { +private fun ZoomableImage(imageUri: Uri, modifier: Modifier = Modifier) { var scale by remember { mutableFloatStateOf(1f) } var offsetX by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) } @@ -298,102 +299,85 @@ private fun ZoomableImage( } ) { AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUri) - .crossfade(true) - .build(), + model = ImageRequest.Builder(LocalContext.current).data(imageUri).crossfade(true).build(), contentDescription = "可缩放图片", contentScale = ContentScale.Fit, - modifier = Modifier - .fillMaxSize() - .graphicsLayer( - scaleX = scale, - scaleY = scale, - translationX = offsetX, - translationY = offsetY - ) + modifier = Modifier.fillMaxSize().graphicsLayer( + scaleX = scale, scaleY = scale, + translationX = offsetX, translationY = offsetY + ) ) } } -@Composable -private fun ImageItem( - uri: Uri, - isSelected: Boolean, - isSelectionMode: Boolean, - onClick: () -> Unit, - onLongClick: () -> Unit -) { - Box( - modifier = Modifier - .aspectRatio(1f) - .padding(2.dp) - .clip(RoundedCornerShape(4.dp)) - .clickable(onClick = onClick) - .then( - if (isSelected) { - Modifier.background(Primary.copy(alpha = 0.3f)) - } else { - Modifier - } - ) - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(uri) - .crossfade(true) - .build(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - - if (isSelected) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = "已选择", - tint = Primary, - modifier = Modifier - .align(Alignment.TopEnd) - .padding(4.dp) - .size(24.dp) - .background(Color.White, RoundedCornerShape(12.dp)) - ) - } - } -} - -private suspend fun loadImagesFromGallery(context: android.content.Context): List { +private suspend fun loadImagesFromGallery(context: android.content.Context): List { return withContext(Dispatchers.IO) { - val images = mutableListOf() - val projection = arrayOf(MediaStore.Images.Media._ID) + val images = mutableListOf() + val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DATE_ADDED) val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC" - - // 只读取checkshot文件夹中的图片 val selection = "${MediaStore.Images.Media.RELATIVE_PATH} LIKE ?" val selectionArgs = arrayOf("%/checkshot/%") context.contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - projection, - selection, - selectionArgs, - sortOrder + projection, selection, selectionArgs, sortOrder )?.use { cursor -> - val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) + val idCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) + val dateCol = try { cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED) } catch (e: Exception) { -1 } while (cursor.moveToNext()) { - val id = cursor.getLong(idColumn) - val uri = Uri.withAppendedPath( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - id.toString() - ) - images.add(uri) + val id = cursor.getLong(idCol) + val dateSec = if (dateCol >= 0) cursor.getLong(dateCol) else 0L + val uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id.toString()) + images.add(GalleryImage(uri, dateSec * 1000L)) } } images } } +private fun categorizeImages(images: List): List { + if (images.isEmpty()) return emptyList() + + val now = Calendar.getInstance() + val today = (now.clone() as Calendar).apply { + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + val startToday = today.timeInMillis + val startYesterday = (today.clone() as Calendar).apply { add(Calendar.DATE, -1) }.timeInMillis + val startLastMonth = (now.clone() as Calendar).apply { + set(Calendar.DAY_OF_MONTH, 1) + add(Calendar.MONTH, -1) + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + + val todayList = mutableListOf() + val yesterdayList = mutableListOf() + val lastMonthList = mutableListOf() + val olderList = mutableListOf() + + for (img in images) { + when { + img.dateAdded >= startToday -> todayList.add(img) + img.dateAdded >= startYesterday -> yesterdayList.add(img) + img.dateAdded >= startLastMonth -> lastMonthList.add(img) + else -> olderList.add(img) + } + } + + val sections = mutableListOf() + if (todayList.isNotEmpty()) sections.add(GallerySection("今天", todayList)) + if (yesterdayList.isNotEmpty()) sections.add(GallerySection("昨天", yesterdayList)) + if (lastMonthList.isNotEmpty()) sections.add(GallerySection("上月", lastMonthList)) + if (olderList.isNotEmpty()) sections.add(GallerySection("更早", olderList)) + return sections +} + private fun deleteImage(context: android.content.Context, uri: Uri) { try { context.contentResolver.delete(uri, null, null) @@ -409,4 +393,4 @@ private fun shareImages(context: android.content.Context, uris: List) { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } context.startActivity(Intent.createChooser(intent, "分享图片")) -} +} \ No newline at end of file