feat(ui): 支持按住跨行滑动连续多选记录

This commit is contained in:
xfy 2026-07-23 14:31:33 +08:00
parent b333c527da
commit 4ada46a549
2 changed files with 105 additions and 4 deletions

View File

@ -140,6 +140,18 @@ class DateRecorderViewModel(
_selectedIds.value = setOf(id) _selectedIds.value = setOf(id)
} }
/**
* 批量设置选中的记录 ID 集合响应按住滑动多选操作
*
* @param ids 选中的记录 ID 集合
*/
fun setSelectedIds(ids: Set<Long>) {
if (!_selectionMode.value) {
_selectionMode.value = true
}
_selectedIds.value = ids
}
/** /**
* 切换某条记录的选中状态 * 切换某条记录的选中状态
*/ */

View File

@ -17,8 +17,10 @@ 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.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
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
@ -30,8 +32,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
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.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -44,7 +48,11 @@ 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.PhotoCamera import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material.icons.filled.Sort import androidx.compose.material.icons.filled.Sort
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import kotlinx.coroutines.launch
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Card import androidx.compose.material3.Card
@ -306,7 +314,8 @@ fun DateRecorderScreen(
photoRoot = repo, photoRoot = repo,
onOpenRecord = onOpenRecord, onOpenRecord = onOpenRecord,
onToggleSelection = viewModel::toggleSelection, onToggleSelection = viewModel::toggleSelection,
onStartSelectionWith = viewModel::startSelectionModeWith onStartSelectionWith = viewModel::startSelectionModeWith,
onSetSelectedIds = viewModel::setSelectedIds
) )
} }
} }
@ -345,11 +354,58 @@ private fun RecordGrid(
photoRoot: DateRecorderRepository, photoRoot: DateRecorderRepository,
onOpenRecord: (Long) -> Unit, onOpenRecord: (Long) -> Unit,
onToggleSelection: (Long) -> Unit, onToggleSelection: (Long) -> Unit,
onStartSelectionWith: (Long) -> Unit onStartSelectionWith: (Long) -> Unit,
onSetSelectedIds: (Set<Long>) -> Unit
) { ) {
val gridState = rememberLazyGridState()
val coroutineScope = rememberCoroutineScope()
var initialDragIndex by remember { mutableStateOf<Int?>(null) }
var initialSelectedIds by remember { mutableStateOf<Set<Long>>(emptySet()) }
LazyVerticalGrid( LazyVerticalGrid(
state = gridState,
columns = GridCells.Adaptive(minSize = 100.dp), columns = GridCells.Adaptive(minSize = 100.dp),
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.pointerInput(records, selectionMode, selectedIds) {
detectDragGesturesAfterLongPress(
onDragStart = { startOffset ->
val index = findItemIndexAtOffset(gridState, startOffset)
if (index != null) {
initialDragIndex = index
initialSelectedIds = if (selectionMode) selectedIds else emptySet()
val startId = records[index].id
onSetSelectedIds(initialSelectedIds + startId)
}
},
onDrag = { change, _ ->
change.consume()
val startIndex = initialDragIndex ?: return@detectDragGesturesAfterLongPress
val currentIndex = findItemIndexAtOffset(gridState, change.position)
?: return@detectDragGesturesAfterLongPress
val minIdx = minOf(startIndex, currentIndex)
val maxIdx = maxOf(startIndex, currentIndex)
val draggedIds = (minIdx..maxIdx).mapNotNull { records.getOrNull(it)?.id }.toSet()
onSetSelectedIds(initialSelectedIds + draggedIds)
// 边界滑动自动滚屏
val viewportHeight = gridState.layoutInfo.viewportSize.height
if (change.position.y < 120f) {
coroutineScope.launch { gridState.scrollBy(-25f) }
} else if (change.position.y > viewportHeight - 120f) {
coroutineScope.launch { gridState.scrollBy(25f) }
}
},
onDragEnd = {
initialDragIndex = null
},
onDragCancel = {
initialDragIndex = null
}
)
},
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
@ -384,6 +440,39 @@ private fun RecordGrid(
} }
} }
/**
* 根据触控点 Offset 命中测试 LazyVerticalGrid 中的 VisibleItem 索引
*/
private fun findItemIndexAtOffset(gridState: LazyGridState, offset: Offset): Int? {
val itemsInfo = gridState.layoutInfo.visibleItemsInfo
if (itemsInfo.isEmpty()) return null
val hit = itemsInfo.firstOrNull { item ->
val x = item.offset.x
val y = item.offset.y
offset.x >= x && offset.x <= x + item.size.width &&
offset.y >= y && offset.y <= y + item.size.height
}
if (hit != null) return hit.index
val firstVisible = itemsInfo.first()
if (offset.y < firstVisible.offset.y) {
return firstVisible.index
}
val lastVisible = itemsInfo.last()
val lastBottom = lastVisible.offset.y + lastVisible.size.height
if (offset.y > lastBottom) {
return lastVisible.index
}
val rowMatch = itemsInfo.minByOrNull { item ->
val itemCenterY = item.offset.y + item.size.height / 2f
kotlin.math.abs(offset.y - itemCenterY)
}
return rowMatch?.index
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun RecordCard( private fun RecordCard(