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.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, "分享图片"))
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user