feat: 相册按日期分组(今天/昨天/上月/更早)
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user