feat: 相册按日期分组(今天/昨天/上月/更早)

This commit is contained in:
Developer
2026-04-21 22:33:56 +08:00
parent 697a6cae87
commit 9455720516

View File

@@ -9,7 +9,6 @@ import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack 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.Close
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Share 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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<GalleryImage>)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun GalleryScreen( fun GalleryScreen(onNavigateBack: () -> Unit) {
onNavigateBack: () -> Unit
) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var images by remember { mutableStateOf<List<Uri>>(emptyList()) } var images by remember { mutableStateOf<List<GalleryImage>>(emptyList()) }
var selectedImages by remember { mutableStateOf<Set<Uri>>(emptySet()) } var selectedImages by remember { mutableStateOf<Set<Uri>>(emptySet()) }
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
var isSelectionMode by remember { mutableStateOf(false) } var isSelectionMode by remember { mutableStateOf(false) }
var selectedImageUri by remember { mutableStateOf<Uri?>(null) } var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
// 加载图片
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
images = loadImagesFromGallery(context) images = loadImagesFromGallery(context)
} }
val sections: List<GallerySection> = remember(images) {
categorizeImages(images)
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(text = if (isSelectionMode) "${selectedImages.size} 张已选择" else "相册")
text = if (isSelectionMode) "${selectedImages.size} 张已选择" else "相册"
)
}, },
navigationIcon = { navigationIcon = {
IconButton(onClick = { IconButton(onClick = {
@@ -128,49 +129,60 @@ fun GalleryScreen(
) { paddingValues -> ) { paddingValues ->
if (images.isEmpty()) { if (images.isEmpty()) {
Box( Box(
modifier = Modifier modifier = Modifier.fillMaxSize().padding(paddingValues),
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text("暂无图片", style = MaterialTheme.typography.bodyLarge) Text("暂无图片", style = MaterialTheme.typography.bodyLarge)
} }
} else { } else {
LazyVerticalGrid( LazyColumn(
columns = GridCells.Fixed(3), modifier = Modifier.fillMaxSize().padding(paddingValues)
contentPadding = PaddingValues(4.dp),
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) { ) {
items(images) { uri -> sections.forEach { section ->
ImageItem( item {
uri = uri, Text(
isSelected = selectedImages.contains(uri), text = section.title,
isSelectionMode = isSelectionMode, style = MaterialTheme.typography.titleMedium,
onClick = { modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp)
if (isSelectionMode) { )
selectedImages = if (selectedImages.contains(uri)) { }
selectedImages - uri val rows = section.images.chunked(3)
} else { items(rows.size) { rowIndex ->
selectedImages + uri 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
} }
}, repeat(3 - row.size) {
onLongClick = { Spacer(modifier = Modifier.weight(1f))
isSelectionMode = true }
selectedImages = selectedImages + uri
} }
) }
item {
Spacer(modifier = Modifier.height(8.dp))
}
} }
} }
} }
} }
// 删除确认对话框
if (showDeleteDialog) { if (showDeleteDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { showDeleteDialog = false }, onDismissRequest = { showDeleteDialog = false },
@@ -199,7 +211,6 @@ fun GalleryScreen(
) )
} }
// 大图查看对话框(支持双指缩放)
if (selectedImageUri != null) { if (selectedImageUri != null) {
var showDeleteConfirm by remember { mutableStateOf(false) } var showDeleteConfirm by remember { mutableStateOf(false) }
@@ -214,11 +225,7 @@ fun GalleryScreen(
Text("图片预览") Text("图片预览")
Row { Row {
IconButton(onClick = { showDeleteConfirm = true }) { IconButton(onClick = { showDeleteConfirm = true }) {
Icon( Icon(Icons.Default.Delete, contentDescription = "删除", tint = Color.Red)
Icons.Default.Delete,
contentDescription = "删除",
tint = Color.Red
)
} }
IconButton(onClick = { selectedImageUri = null }) { IconButton(onClick = { selectedImageUri = null }) {
Icon(Icons.Default.Close, contentDescription = "关闭") Icon(Icons.Default.Close, contentDescription = "关闭")
@@ -229,9 +236,7 @@ fun GalleryScreen(
text = { text = {
ZoomableImage( ZoomableImage(
imageUri = selectedImageUri!!, imageUri = selectedImageUri!!,
modifier = Modifier modifier = Modifier.fillMaxWidth().aspectRatio(1f)
.fillMaxWidth()
.aspectRatio(1f)
) )
}, },
confirmButton = { confirmButton = {
@@ -241,7 +246,6 @@ fun GalleryScreen(
} }
) )
// 删除二次确认对话框
if (showDeleteConfirm) { if (showDeleteConfirm) {
AlertDialog( AlertDialog(
onDismissRequest = { showDeleteConfirm = false }, onDismissRequest = { showDeleteConfirm = false },
@@ -270,10 +274,7 @@ fun GalleryScreen(
} }
@Composable @Composable
private fun ZoomableImage( private fun ZoomableImage(imageUri: Uri, modifier: Modifier = Modifier) {
imageUri: Uri,
modifier: Modifier = Modifier
) {
var scale by remember { mutableFloatStateOf(1f) } var scale by remember { mutableFloatStateOf(1f) }
var offsetX by remember { mutableFloatStateOf(0f) } var offsetX by remember { mutableFloatStateOf(0f) }
var offsetY by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) }
@@ -298,102 +299,85 @@ private fun ZoomableImage(
} }
) { ) {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(LocalContext.current) model = ImageRequest.Builder(LocalContext.current).data(imageUri).crossfade(true).build(),
.data(imageUri)
.crossfade(true)
.build(),
contentDescription = "可缩放图片", contentDescription = "可缩放图片",
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
modifier = Modifier modifier = Modifier.fillMaxSize().graphicsLayer(
.fillMaxSize() scaleX = scale, scaleY = scale,
.graphicsLayer( translationX = offsetX, translationY = offsetY
scaleX = scale, )
scaleY = scale,
translationX = offsetX,
translationY = offsetY
)
) )
} }
} }
@Composable private suspend fun loadImagesFromGallery(context: android.content.Context): List<GalleryImage> {
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<Uri> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val images = mutableListOf<Uri>() val images = mutableListOf<GalleryImage>()
val projection = arrayOf(MediaStore.Images.Media._ID) val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DATE_ADDED)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC" val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
// 只读取checkshot文件夹中的图片
val selection = "${MediaStore.Images.Media.RELATIVE_PATH} LIKE ?" val selection = "${MediaStore.Images.Media.RELATIVE_PATH} LIKE ?"
val selectionArgs = arrayOf("%/checkshot/%") val selectionArgs = arrayOf("%/checkshot/%")
context.contentResolver.query( context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, projection, selection, selectionArgs, sortOrder
selection,
selectionArgs,
sortOrder
)?.use { cursor -> )?.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()) { while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn) val id = cursor.getLong(idCol)
val uri = Uri.withAppendedPath( val dateSec = if (dateCol >= 0) cursor.getLong(dateCol) else 0L
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, val uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id.toString())
id.toString() images.add(GalleryImage(uri, dateSec * 1000L))
)
images.add(uri)
} }
} }
images images
} }
} }
private fun categorizeImages(images: List<GalleryImage>): List<GallerySection> {
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<GalleryImage>()
val yesterdayList = mutableListOf<GalleryImage>()
val lastMonthList = mutableListOf<GalleryImage>()
val olderList = mutableListOf<GalleryImage>()
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<GallerySection>()
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) { private fun deleteImage(context: android.content.Context, uri: Uri) {
try { try {
context.contentResolver.delete(uri, null, null) context.contentResolver.delete(uri, null, null)
@@ -409,4 +393,4 @@ private fun shareImages(context: android.content.Context, uris: List<Uri>) {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
context.startActivity(Intent.createChooser(intent, "分享图片")) context.startActivity(Intent.createChooser(intent, "分享图片"))
} }