feat(date-recorder): 美化记录器界面,卡片图上叠加文字、排序菜单精简
- 网格卡片:照片铺满整卡,底部渐变蒙层叠加标题与拍摄日期 - 网格改为自适应约 3 列,间距 8dp→4dp,卡片圆角 12dp→8dp - 排序菜单 6 项精简为 3 个字段项,点击已选字段切换升降序 - 新增 RecordSortOrder.nextAfter 纯函数及单元测试
This commit is contained in:
parent
b589800eda
commit
d47a38bc19
@ -22,6 +22,21 @@ data class RecordSortOrder(
|
||||
companion object {
|
||||
/** 默认按拍摄日期降序(最新的在前) */
|
||||
val DEFAULT = RecordSortOrder(RecordSortField.SHOOT_DATE, ascending = false)
|
||||
|
||||
/**
|
||||
* 计算用户在排序菜单中点击某字段后的下一个排序方式。
|
||||
*
|
||||
* 点击当前已选字段时翻转方向,点击其他字段时切换字段并保持方向不变。
|
||||
*
|
||||
* @param current 当前排序方式
|
||||
* @param field 用户点击的排序字段
|
||||
*/
|
||||
fun nextAfter(current: RecordSortOrder, field: RecordSortField): RecordSortOrder =
|
||||
if (current.field == field) {
|
||||
current.copy(ascending = !current.ascending)
|
||||
} else {
|
||||
RecordSortOrder(field, current.ascending)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,8 +194,9 @@ class DateRecorderViewModel(
|
||||
RecordSortField.LINKED_DATE -> compareBy(nullsLast()) { it.linkedDate }
|
||||
RecordSortField.CREATED_AT -> compareBy { it.createdAt }
|
||||
}
|
||||
val ordered = if (order.ascending) comparator else comparator.reversed()
|
||||
return records.sortedWith(ordered.then(compareBy { it.id }))
|
||||
// id 兜底随主排序一起反转,保证降序时同值记录也是最新(id 最大)的在前
|
||||
val ordered = comparator.then(compareBy { it.id })
|
||||
return records.sortedWith(if (order.ascending) ordered else ordered.reversed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Checklist
|
||||
import androidx.compose.material.icons.filled.ChevronLeft
|
||||
@ -49,12 +51,14 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@ -73,7 +77,7 @@ import plus.rua.project.RecordSortOrder
|
||||
* 功能:
|
||||
* - 右下角浮动按钮拍摄新照片创建记录
|
||||
* - 点击单条记录进入详情页
|
||||
* - TopAppBar 右侧"排序"菜单:6 种排序组合
|
||||
* - TopAppBar 右侧"排序"菜单:3 个排序字段,点击已选字段切换升降序
|
||||
* - TopAppBar 右侧"多选"按钮进入多选态,可批量删除
|
||||
*
|
||||
* @param onBack 返回回调
|
||||
@ -142,16 +146,39 @@ fun DateRecorderScreen(
|
||||
expanded = showSortMenu,
|
||||
onDismissRequest = { showSortMenu = false }
|
||||
) {
|
||||
sortMenuItems().forEach { item ->
|
||||
sortFields.forEach { (field, label) ->
|
||||
val selected = uiState.sortOrder.field == field
|
||||
DropdownMenuItem(
|
||||
text = { Text(item.label) },
|
||||
text = { Text(label) },
|
||||
onClick = {
|
||||
viewModel.setSortOrder(item.order)
|
||||
viewModel.setSortOrder(
|
||||
RecordSortOrder.nextAfter(uiState.sortOrder, field)
|
||||
)
|
||||
showSortMenu = false
|
||||
},
|
||||
leadingIcon = if (uiState.sortOrder == item.order) {
|
||||
leadingIcon = if (selected) {
|
||||
{ Icon(Icons.Filled.Check, contentDescription = null) }
|
||||
} else null
|
||||
} else {
|
||||
null
|
||||
},
|
||||
trailingIcon = if (selected) {
|
||||
{
|
||||
Icon(
|
||||
imageVector = if (uiState.sortOrder.ascending) {
|
||||
Icons.Filled.ArrowUpward
|
||||
} else {
|
||||
Icons.Filled.ArrowDownward
|
||||
},
|
||||
contentDescription = if (uiState.sortOrder.ascending) {
|
||||
"旧→新"
|
||||
} else {
|
||||
"新→旧"
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -248,15 +275,11 @@ fun DateRecorderScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private data class SortMenuItem(val order: RecordSortOrder, val label: String)
|
||||
|
||||
private fun sortMenuItems(): List<SortMenuItem> = listOf(
|
||||
SortMenuItem(RecordSortOrder(RecordSortField.SHOOT_DATE, false), "拍摄日期 · 新→旧"),
|
||||
SortMenuItem(RecordSortOrder(RecordSortField.SHOOT_DATE, true), "拍摄日期 · 旧→新"),
|
||||
SortMenuItem(RecordSortOrder(RecordSortField.LINKED_DATE, false), "关联日期 · 新→旧"),
|
||||
SortMenuItem(RecordSortOrder(RecordSortField.LINKED_DATE, true), "关联日期 · 旧→新"),
|
||||
SortMenuItem(RecordSortOrder(RecordSortField.CREATED_AT, false), "创建时间 · 新→旧"),
|
||||
SortMenuItem(RecordSortOrder(RecordSortField.CREATED_AT, true), "创建时间 · 旧→新")
|
||||
/** 排序菜单展示的字段选项,点击已选字段时切换方向(见 [RecordSortOrder.nextAfter])。 */
|
||||
private val sortFields = listOf(
|
||||
RecordSortField.SHOOT_DATE to "拍摄日期",
|
||||
RecordSortField.LINKED_DATE to "关联日期",
|
||||
RecordSortField.CREATED_AT to "创建时间"
|
||||
)
|
||||
|
||||
@Composable
|
||||
@ -269,11 +292,11 @@ private fun RecordGrid(
|
||||
onToggleSelection: (Long) -> Unit
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
columns = GridCells.Adaptive(minSize = 100.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(records, key = { it.id }) { record ->
|
||||
RecordCard(
|
||||
@ -301,35 +324,46 @@ private fun RecordCard(
|
||||
) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
Box {
|
||||
Column {
|
||||
AsyncImage(
|
||||
uri = photoUri,
|
||||
contentDescription = record.title,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||
AsyncImage(
|
||||
uri = photoUri,
|
||||
contentDescription = record.title,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
)
|
||||
|
||||
// 底部渐变蒙层,保证叠加文字在各种照片上的可读性
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = 0.65f)
|
||||
)
|
||||
)
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = record.title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = Color.White,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = "${record.shootDate}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.White.copy(alpha = 0.85f)
|
||||
)
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = record.title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1
|
||||
)
|
||||
Text(
|
||||
text = "${record.shootDate}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 多选态下的复选角标
|
||||
|
||||
@ -76,6 +76,20 @@ class DateRecorderSortTest {
|
||||
assertEquals(listOf(1L, 2L, 3L), sorted.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sortByShootDate_descending_sameDate_newestIdFirst() {
|
||||
// 同一天拍摄多条记录,降序时最新拍摄(id 最大)应排在最前
|
||||
val sameDay = listOf(
|
||||
record(id = 1, shoot = LocalDate(2026, 7, 20), created = Instant.fromEpochSeconds(1000)),
|
||||
record(id = 2, shoot = LocalDate(2026, 7, 20), created = Instant.fromEpochSeconds(2000))
|
||||
)
|
||||
val sorted = DateRecorderViewModel.sortDateRecords(
|
||||
sameDay,
|
||||
RecordSortOrder(RecordSortField.SHOOT_DATE, ascending = false)
|
||||
)
|
||||
assertEquals(listOf(2L, 1L), sorted.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sort_emptyList_returnsEmpty() {
|
||||
val sorted = DateRecorderViewModel.sortDateRecords(
|
||||
@ -101,6 +115,24 @@ class DateRecorderSortTest {
|
||||
assertEquals(listOf(2L, 5L, 8L), sorted.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nextAfter_sameField_togglesDirection() {
|
||||
val current = RecordSortOrder(RecordSortField.SHOOT_DATE, ascending = false)
|
||||
assertEquals(
|
||||
RecordSortOrder(RecordSortField.SHOOT_DATE, ascending = true),
|
||||
RecordSortOrder.nextAfter(current, RecordSortField.SHOOT_DATE)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nextAfter_otherField_switchesFieldKeepsDirection() {
|
||||
val current = RecordSortOrder(RecordSortField.SHOOT_DATE, ascending = true)
|
||||
assertEquals(
|
||||
RecordSortOrder(RecordSortField.CREATED_AT, ascending = true),
|
||||
RecordSortOrder.nextAfter(current, RecordSortField.CREATED_AT)
|
||||
)
|
||||
}
|
||||
|
||||
private fun record(
|
||||
id: Long,
|
||||
title: String = "t",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user