feat(ui): 支持长按单个记录直接进入多选模式并选中
This commit is contained in:
parent
cfac607862
commit
a2a631c141
@ -130,6 +130,16 @@ class DateRecorderViewModel(
|
||||
_selectedIds.value = emptySet()
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启多选模式并选中指定的记录 ID(响应长按操作)。
|
||||
*
|
||||
* @param id 长按选中的记录 ID
|
||||
*/
|
||||
fun startSelectionModeWith(id: Long) {
|
||||
_selectionMode.value = true
|
||||
_selectedIds.value = setOf(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换某条记录的选中状态。
|
||||
*/
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user