feat(ui): 支持长按单个记录直接进入多选模式并选中
This commit is contained in:
parent
cfac607862
commit
a2a631c141
@ -130,6 +130,16 @@ class DateRecorderViewModel(
|
|||||||
_selectedIds.value = emptySet()
|
_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.slideOutVertically
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@ -303,7 +305,8 @@ fun DateRecorderScreen(
|
|||||||
selectionMode = uiState.selectionMode,
|
selectionMode = uiState.selectionMode,
|
||||||
photoRoot = repo,
|
photoRoot = repo,
|
||||||
onOpenRecord = onOpenRecord,
|
onOpenRecord = onOpenRecord,
|
||||||
onToggleSelection = viewModel::toggleSelection
|
onToggleSelection = viewModel::toggleSelection,
|
||||||
|
onStartSelectionWith = viewModel::startSelectionModeWith
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -341,7 +344,8 @@ private fun RecordGrid(
|
|||||||
selectionMode: Boolean,
|
selectionMode: Boolean,
|
||||||
photoRoot: DateRecorderRepository,
|
photoRoot: DateRecorderRepository,
|
||||||
onOpenRecord: (Long) -> Unit,
|
onOpenRecord: (Long) -> Unit,
|
||||||
onToggleSelection: (Long) -> Unit
|
onToggleSelection: (Long) -> Unit,
|
||||||
|
onStartSelectionWith: (Long) -> Unit
|
||||||
) {
|
) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 100.dp),
|
columns = GridCells.Adaptive(minSize = 100.dp),
|
||||||
@ -359,12 +363,20 @@ private fun RecordGrid(
|
|||||||
onClick = {
|
onClick = {
|
||||||
if (selectionMode) onToggleSelection(record.id)
|
if (selectionMode) onToggleSelection(record.id)
|
||||||
else onOpenRecord(record.id)
|
else onOpenRecord(record.id)
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
if (selectionMode) {
|
||||||
|
onToggleSelection(record.id)
|
||||||
|
} else {
|
||||||
|
onStartSelectionWith(record.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun RecordCard(
|
private fun RecordCard(
|
||||||
record: DateRecord,
|
record: DateRecord,
|
||||||
@ -372,6 +384,7 @@ private fun RecordCard(
|
|||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
selectionMode: Boolean,
|
selectionMode: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val cardScale by animateFloatAsState(
|
val cardScale by animateFloatAsState(
|
||||||
@ -389,9 +402,9 @@ private fun RecordCard(
|
|||||||
animationSpec = tween(200),
|
animationSpec = tween(200),
|
||||||
label = "record_card_border_color"
|
label = "record_card_border_color"
|
||||||
)
|
)
|
||||||
|
val cardShape = RoundedCornerShape(8.dp)
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick,
|
shape = cardShape,
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
border = BorderStroke(borderWidth, borderColor),
|
border = BorderStroke(borderWidth, borderColor),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@ -400,6 +413,11 @@ private fun RecordCard(
|
|||||||
scaleX = cardScale
|
scaleX = cardScale
|
||||||
scaleY = cardScale
|
scaleY = cardScale
|
||||||
}
|
}
|
||||||
|
.clip(cardShape)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
AsyncImage(
|
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