|
|
@@ -16,8 +16,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedback
|
|
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
|
import androidx.compose.ui.input.pointer.pointerInput
|
|
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
|
|
-import kotlinx.coroutines.coroutineScope
|
|
|
-import kotlinx.coroutines.launch
|
|
|
+import androidx.compose.ui.util.fastFirstOrNull
|
|
|
|
|
|
@Composable
|
|
|
fun <T, ID> DraggableSelectionHandler(
|
|
|
@@ -30,69 +29,87 @@ fun <T, ID> DraggableSelectionHandler(
|
|
|
haptic: HapticFeedback = LocalHapticFeedback.current,
|
|
|
) {
|
|
|
val latestSelectedIds by rememberUpdatedState(selectedIds)
|
|
|
+ val latestOnSelectionChange by rememberUpdatedState(onSelectionChange)
|
|
|
+ val latestIdProvider by rememberUpdatedState(idProvider)
|
|
|
+ val latestItems by rememberUpdatedState(items)
|
|
|
+
|
|
|
var isAddingMode by remember { mutableStateOf(true) }
|
|
|
var lastProcessedIndex by remember { mutableIntStateOf(-1) }
|
|
|
|
|
|
- fun findItemAtOffset(offsetY: Float): Pair<Int, T>? {
|
|
|
- val itemInfo = listState.layoutInfo.visibleItemsInfo
|
|
|
- .firstOrNull { item ->
|
|
|
- offsetY >= item.offset && offsetY <= item.offset + item.size
|
|
|
- }
|
|
|
+ fun findItemAtOffset(offsetY: Float): Pair<Int, ID>? {
|
|
|
+ val layoutInfo = listState.layoutInfo
|
|
|
+ val itemsInfo = layoutInfo.visibleItemsInfo
|
|
|
+ if (itemsInfo.isEmpty()) return null
|
|
|
|
|
|
- return itemInfo?.let { info ->
|
|
|
- items.getOrNull(info.index)?.let { item ->
|
|
|
- info.index to item
|
|
|
- }
|
|
|
+ // 核心修复:校准坐标系。
|
|
|
+ // offsetY 是相对于此 Box 顶部的。
|
|
|
+ // item.offset 是相对于视口起始位置(通常包含 contentPadding)的。
|
|
|
+ // 当开启模糊时,LazyColumn 占据全屏但有 topContentPadding。
|
|
|
+ // item.offset 在 Compose 的 LazyListLayoutInfo 中是相对于“内容区域”开始计算的。
|
|
|
+ val adjustedY = offsetY - layoutInfo.beforeContentPadding
|
|
|
+
|
|
|
+ val itemInfo = itemsInfo.fastFirstOrNull { item ->
|
|
|
+ adjustedY >= item.offset && adjustedY <= (item.offset + item.size)
|
|
|
+ } ?: return null
|
|
|
+
|
|
|
+ @Suppress("UNCHECKED_CAST")
|
|
|
+ val id = try {
|
|
|
+ itemInfo.key as ID
|
|
|
+ } catch (e: Exception) {
|
|
|
+ latestItems.getOrNull(itemInfo.index)?.let { latestIdProvider(it) } ?: return null
|
|
|
}
|
|
|
+
|
|
|
+ return itemInfo.index to id
|
|
|
}
|
|
|
|
|
|
fun applySelection(id: ID, add: Boolean) {
|
|
|
val current = latestSelectedIds
|
|
|
- onSelectionChange(
|
|
|
- if (add) current + id else current - id
|
|
|
- )
|
|
|
+ if (add) {
|
|
|
+ if (!current.contains(id)) {
|
|
|
+ latestOnSelectionChange(current + id)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (current.contains(id)) {
|
|
|
+ latestOnSelectionChange(current - id)
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
Box(
|
|
|
- modifier = modifier.pointerInput(Unit) {
|
|
|
- coroutineScope {
|
|
|
- launch {
|
|
|
- detectTapGestures(
|
|
|
- onTap = { offset ->
|
|
|
- findItemAtOffset(offset.y)?.let { (_, item) ->
|
|
|
- val id = idProvider(item)
|
|
|
- applySelection(id, !latestSelectedIds.contains(id))
|
|
|
- haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
|
|
- }
|
|
|
+ modifier = modifier
|
|
|
+ .pointerInput(listState) {
|
|
|
+ detectTapGestures(
|
|
|
+ onTap = { offset ->
|
|
|
+ findItemAtOffset(offset.y)?.let { (_, id) ->
|
|
|
+ applySelection(id, !latestSelectedIds.contains(id))
|
|
|
+ haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
|
|
}
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- launch {
|
|
|
- detectDragGestures(
|
|
|
- onDragStart = { offset ->
|
|
|
- findItemAtOffset(offset.y)?.let { (index, item) ->
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .pointerInput(listState) {
|
|
|
+ detectDragGestures(
|
|
|
+ onDragStart = { offset ->
|
|
|
+ findItemAtOffset(offset.y)?.let { (index, id) ->
|
|
|
+ lastProcessedIndex = index
|
|
|
+ isAddingMode = !latestSelectedIds.contains(id)
|
|
|
+ applySelection(id, isAddingMode)
|
|
|
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onDrag = { change, _ ->
|
|
|
+ findItemAtOffset(change.position.y)?.let { (index, id) ->
|
|
|
+ if (index != lastProcessedIndex) {
|
|
|
lastProcessedIndex = index
|
|
|
- val id = idProvider(item)
|
|
|
- isAddingMode = !latestSelectedIds.contains(id)
|
|
|
applySelection(id, isAddingMode)
|
|
|
- haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
|
- }
|
|
|
- },
|
|
|
- onDrag = { change, _ ->
|
|
|
- findItemAtOffset(change.position.y)?.let { (index, item) ->
|
|
|
- if (index != lastProcessedIndex) {
|
|
|
- lastProcessedIndex = index
|
|
|
- applySelection(idProvider(item), isAddingMode)
|
|
|
- haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
|
|
- }
|
|
|
+ haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
|
|
}
|
|
|
- },
|
|
|
- onDragEnd = { lastProcessedIndex = -1 },
|
|
|
- onDragCancel = { lastProcessedIndex = -1 }
|
|
|
- )
|
|
|
- }
|
|
|
+ }
|
|
|
+ if (change.pressed) change.consume()
|
|
|
+ },
|
|
|
+ onDragEnd = { lastProcessedIndex = -1 },
|
|
|
+ onDragCancel = { lastProcessedIndex = -1 }
|
|
|
+ )
|
|
|
}
|
|
|
- }
|
|
|
)
|
|
|
-}
|
|
|
+}
|