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.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<GalleryImage>)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GalleryScreen(
onNavigateBack: () -> Unit
) {
fun GalleryScreen(onNavigateBack: () -> Unit) {
val context = LocalContext.current
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 showDeleteDialog by remember { mutableStateOf(false) }
var isSelectionMode by remember { mutableStateOf(false) }
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
// 加载图片
LaunchedEffect(Unit) {
images = loadImagesFromGallery(context)
}
val sections: List<GallerySection> = 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
}
} else {
// 非选择模式下,点击打开大图查看器
selectedImageUri = uri
}
},
onLongClick = {
isSelectionMode = true
selectedImages = 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()
)
}
}
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<Uri> {
private suspend fun loadImagesFromGallery(context: android.content.Context): List<GalleryImage> {
return withContext(Dispatchers.IO) {
val images = mutableListOf<Uri>()
val projection = arrayOf(MediaStore.Images.Media._ID)
val images = mutableListOf<GalleryImage>()
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<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) {
try {
context.contentResolver.delete(uri, null, null)