feat(ui): 支持长按单个记录直接进入多选模式并选中

This commit is contained in:
xfy 2026-07-23 14:26:19 +08:00
parent cfac607862
commit a2a631c141
3 changed files with 119 additions and 4 deletions

View File

@ -130,6 +130,16 @@ class DateRecorderViewModel(
_selectedIds.value = emptySet()
}
/**
* 开启多选模式并选中指定的记录 ID响应长按操作
*
* @param id 长按选中的记录 ID
*/
fun startSelectionModeWith(id: Long) {
_selectionMode.value = true
_selectedIds.value = setOf(id)
}
/**
* 切换某条记录的选中状态
*/

View File

@ -16,6 +16,8 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -303,7 +305,8 @@ fun DateRecorderScreen(
selectionMode = uiState.selectionMode,
photoRoot = repo,
onOpenRecord = onOpenRecord,
onToggleSelection = viewModel::toggleSelection
onToggleSelection = viewModel::toggleSelection,
onStartSelectionWith = viewModel::startSelectionModeWith
)
}
}
@ -341,7 +344,8 @@ private fun RecordGrid(
selectionMode: Boolean,
photoRoot: DateRecorderRepository,
onOpenRecord: (Long) -> Unit,
onToggleSelection: (Long) -> Unit
onToggleSelection: (Long) -> Unit,
onStartSelectionWith: (Long) -> Unit
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 100.dp),
@ -359,12 +363,20 @@ private fun RecordGrid(
onClick = {
if (selectionMode) onToggleSelection(record.id)
else onOpenRecord(record.id)
},
onLongClick = {
if (selectionMode) {
onToggleSelection(record.id)
} else {
onStartSelectionWith(record.id)
}
}
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun RecordCard(
record: DateRecord,
@ -372,6 +384,7 @@ private fun RecordCard(
isSelected: Boolean,
selectionMode: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier
) {
val cardScale by animateFloatAsState(
@ -389,9 +402,9 @@ private fun RecordCard(
animationSpec = tween(200),
label = "record_card_border_color"
)
val cardShape = RoundedCornerShape(8.dp)
Card(
onClick = onClick,
shape = RoundedCornerShape(8.dp),
shape = cardShape,
border = BorderStroke(borderWidth, borderColor),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
modifier = modifier
@ -400,6 +413,11 @@ private fun RecordCard(
scaleX = cardScale
scaleY = cardScale
}
.clip(cardShape)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
)
) {
Box {
AsyncImage(

View File

@ -0,0 +1,87 @@
package plus.rua.project
import java.io.File
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
/**
* [DateRecorderViewModel] 的多选模式与选择状态单元测试
*/
@OptIn(ExperimentalCoroutinesApi::class)
class DateRecorderViewModelTest {
private val testDispatcher = UnconfinedTestDispatcher()
@BeforeTest
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@AfterTest
fun tearDown() {
Dispatchers.resetMain()
}
private fun newViewModel(): DateRecorderViewModel {
val repository = DateRecorderRepository(FakeDateRecorderDao(), File("build/tmp/date_recorder_vm_test"))
return DateRecorderViewModel(repository)
}
@Test
fun startSelectionModeWith_enablesSelectionMode_andSelectsSpecifiedId() = runTest(testDispatcher) {
val vm = newViewModel()
backgroundScope.launch { vm.uiState.collect {} }
assertFalse(vm.uiState.value.selectionMode)
vm.startSelectionModeWith(42L)
assertTrue(vm.uiState.value.selectionMode)
assertEquals(setOf(42L), vm.uiState.value.selectedIds)
}
@Test
fun toggleSelectionMode_togglesState_andClearsSelection() = runTest(testDispatcher) {
val vm = newViewModel()
backgroundScope.launch { vm.uiState.collect {} }
vm.startSelectionModeWith(100L)
assertTrue(vm.uiState.value.selectionMode)
vm.toggleSelectionMode()
assertFalse(vm.uiState.value.selectionMode)
assertTrue(vm.uiState.value.selectedIds.isEmpty())
}
@Test
fun toggleSelection_addsAndRemovesId() = runTest(testDispatcher) {
val vm = newViewModel()
backgroundScope.launch { vm.uiState.collect {} }
vm.startSelectionModeWith(1L)
vm.toggleSelection(2L)
assertEquals(setOf(1L, 2L), vm.uiState.value.selectedIds)
vm.toggleSelection(1L)
assertEquals(setOf(2L), vm.uiState.value.selectedIds)
}
}
private class FakeDateRecorderDao : DateRecordDao {
override fun getByIdFlow(id: Long): Flow<DateRecord?> = flowOf(null)
override fun getAllFlow(): Flow<List<DateRecord>> = flowOf(emptyList())
override suspend fun insert(record: DateRecord): Long = 0
override suspend fun update(record: DateRecord) = Unit
override suspend fun deleteByIds(ids: List<Long>): Int = 0
}