Skip to content

Commit d70e849

Browse files
authored
Fix: Non-Congruant selection (#27)
Fixes #20
1 parent d56e1ac commit d70e849

File tree

7 files changed

+128
-52
lines changed

7 files changed

+128
-52
lines changed

core/api/android/core.api

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
public final class com/dragselectcompose/core/DragSelectState {
22
public static final field $stable I
3-
public fun <init> (Landroidx/compose/foundation/lazy/grid/LazyGridState;ILjava/util/List;)V
3+
public fun <init> (Landroidx/compose/foundation/lazy/grid/LazyGridState;ILjava/util/List;I)V
4+
public synthetic fun <init> (Landroidx/compose/foundation/lazy/grid/LazyGridState;ILjava/util/List;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
5+
public fun <init> (Landroidx/compose/foundation/lazy/grid/LazyGridState;Ljava/util/List;)V
46
public final fun addSelected (Ljava/lang/Object;)V
57
public final fun clear ()V
68
public final fun getGridState ()Landroidx/compose/foundation/lazy/grid/LazyGridState;

core/api/desktop/core.api

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
public final class com/dragselectcompose/core/DragSelectState {
22
public static final field $stable I
3-
public fun <init> (Landroidx/compose/foundation/lazy/grid/LazyGridState;ILjava/util/List;)V
3+
public fun <init> (Landroidx/compose/foundation/lazy/grid/LazyGridState;ILjava/util/List;I)V
4+
public synthetic fun <init> (Landroidx/compose/foundation/lazy/grid/LazyGridState;ILjava/util/List;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
5+
public fun <init> (Landroidx/compose/foundation/lazy/grid/LazyGridState;Ljava/util/List;)V
46
public final fun addSelected (Ljava/lang/Object;)V
57
public final fun clear ()V
68
public final fun getGridState ()Landroidx/compose/foundation/lazy/grid/LazyGridState;

core/src/commonMain/kotlin/com/dragselectcompose/core/DragSelectState.kt

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import androidx.compose.runtime.mutableStateOf
88
import androidx.compose.runtime.remember
99
import androidx.compose.runtime.saveable.rememberSaveable
1010
import androidx.compose.runtime.setValue
11-
import com.dragselectcompose.core.DragSelectState.Companion.None
11+
import com.dragselectcompose.core.DragState.Companion.None
1212

1313
/**
1414
* Creates a [DragSelectState] that is remembered across compositions.
@@ -26,9 +26,9 @@ public fun <Item> rememberDragSelectState(
2626
lazyGridState: LazyGridState = rememberLazyGridState(),
2727
initialSelection: List<Item> = emptyList(),
2828
): DragSelectState<Item> {
29-
val indexes by rememberSaveable { mutableStateOf(None) }
29+
val indexes by rememberSaveable { mutableStateOf(None to None) }
3030
return remember(lazyGridState) {
31-
DragSelectState(lazyGridState, indexes, initialSelection)
31+
DragSelectState(lazyGridState, indexes.first, initialSelection, indexes.second)
3232
}
3333
}
3434

@@ -40,13 +40,23 @@ public fun <Item> rememberDragSelectState(
4040
* @param[Item] The type of the items in the list.
4141
* @param[gridState] The [LazyGridState] that will be used to control the items in the grid.
4242
* @param[initialIndex] The initial index of the item that was long pressed.
43+
* @param[initialSelection] The initial selection of items.
44+
* @param[currentIndex] The current index of the item that is being dragged over.
4345
*/
4446
public class DragSelectState<Item>(
4547
public val gridState: LazyGridState,
46-
internal var initialIndex: Int,
48+
initialIndex: Int,
4749
initialSelection: List<Item>,
50+
currentIndex: Int = initialIndex,
4851
) {
4952

53+
public constructor(
54+
gridState: LazyGridState,
55+
initialSelection: List<Item>,
56+
) : this(gridState, None, initialSelection, None)
57+
58+
private var dragState: DragState = DragState(initialIndex, currentIndex)
59+
5060
/**
5161
* The state containing the selected items.
5262
*/
@@ -69,14 +79,23 @@ public class DragSelectState<Item>(
6979
/**
7080
* Will only invoke [block] if the initial index is not [None]. Meaning we are in selection mode.
7181
*/
72-
internal fun withInitialIndex(
73-
block: DragSelectState<Item>.(initial: Int) -> Unit,
82+
internal fun whenDragging(
83+
block: DragSelectState<Item>.(dragState: DragState) -> Unit,
7484
) {
75-
if (initialIndex != None) {
76-
block(this, initialIndex)
85+
if (dragState.isDragging) {
86+
block(this, dragState)
7787
}
7888
}
7989

90+
internal fun updateDrag(current: Int) {
91+
dragState = dragState.copy(current = current)
92+
}
93+
94+
internal fun startDrag(item: Item, index: Int) {
95+
dragState = DragState(index, index)
96+
addSelected(item)
97+
}
98+
8099
/**
81100
* Whether or not the provided item is selected.
82101
*
@@ -122,13 +141,8 @@ public class DragSelectState<Item>(
122141
/**
123142
* Resets the drag state.
124143
*/
125-
internal fun resetDrag() {
126-
initialIndex = None
144+
internal fun stopDrag() {
145+
dragState = dragState.copy(initial = DragState.None)
127146
autoScrollSpeed.value = 0f
128147
}
129-
130-
internal companion object {
131-
132-
internal const val None = -1
133-
}
134148
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.dragselectcompose.core
2+
3+
/**
4+
* Represents the current state of a drag gesture.
5+
*
6+
* @param[initial] The index of the item where the drag gesture started.
7+
* @param[current] The index of the item where the drag gesture is currently at.
8+
*/
9+
internal data class DragState(
10+
val initial: Int,
11+
val current: Int,
12+
) {
13+
14+
internal val isDragging: Boolean
15+
get() = initial != None && current != None
16+
17+
internal companion object {
18+
19+
internal const val None = -1
20+
}
21+
}

core/src/commonMain/kotlin/com/dragselectcompose/core/GridDragSelect.kt

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -71,37 +71,46 @@ public fun <Item> Modifier.gridDragSelect(
7171
if (!enableHaptics) null
7272
else hapticFeedback ?: GridDragSelectDefaults.hapticsFeedback
7373

74+
val isSelected: (Item) -> Boolean = { item ->
75+
state.selected.contains(item)
76+
}
77+
7478
pointerInput(Unit) {
7579
detectDragGesturesAfterLongPress(
7680
onDragStart = { offset ->
7781
state.gridState.itemIndexAtPosition(offset)?.let { startIndex ->
7882
val item = items.getOrNull(startIndex)
7983
if (item != null && state.selected.contains(item).not()) {
8084
haptics?.performHapticFeedback(HapticFeedbackType.LongPress)
81-
state.initialIndex = startIndex
82-
state.addSelected(item)
85+
state.startDrag(item, startIndex)
8386
}
8487
}
8588
},
86-
onDragCancel = state::resetDrag,
87-
onDragEnd = state::resetDrag,
89+
onDragCancel = state::stopDrag,
90+
onDragEnd = state::stopDrag,
8891
onDrag = { change, _ ->
89-
state.withInitialIndex { initial ->
92+
state.whenDragging { dragState ->
9093
autoScrollSpeed.value = gridState.calculateScrollSpeed(change, scrollThreshold)
9194

92-
val newSelected =
93-
gridState.getSelectedByPosition(items, initial, change)
94-
?: gridState.getOverscrollItems(items, initial, change)
95+
val itemPosition = gridState.getItemPosition(change.position)
96+
?: return@whenDragging
9597

96-
if (newSelected != null) {
97-
updateSelected(newSelected)
98-
}
98+
val newSelection = items.getSelectedItems(itemPosition, dragState, isSelected)
99+
updateDrag(current = itemPosition)
100+
updateSelected(newSelection)
99101
}
100102
},
101103
)
102104
}
103105
}
104106

107+
/**
108+
* Calculates the auto-scroll speed based on the current drag position.
109+
*
110+
* @param[change] The [PointerInputChange] for the current drag position.
111+
* @param[scrollThreshold] The distance from the edge of the grid to start auto-scrolling.
112+
* @return The auto-scroll speed.
113+
*/
105114
private fun LazyGridState.calculateScrollSpeed(
106115
change: PointerInputChange,
107116
scrollThreshold: Float,
@@ -116,6 +125,12 @@ private fun LazyGridState.calculateScrollSpeed(
116125
}
117126
}
118127

128+
/**
129+
* Gets the index of the item that was hit by the drag.
130+
*
131+
* @param[hitPoint] The point where the drag hit the grid.
132+
* @return The index of the item that was hit by the drag.
133+
*/
119134
private fun LazyGridState.itemIndexAtPosition(hitPoint: Offset): Int? {
120135
val found = layoutInfo.visibleItemsInfo.find { itemInfo ->
121136
itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
@@ -124,36 +139,58 @@ private fun LazyGridState.itemIndexAtPosition(hitPoint: Offset): Int? {
124139
return found?.index
125140
}
126141

127-
private fun <Item> LazyGridState.getSelectedByPosition(
128-
items: List<Item>,
129-
initialIndex: Int,
130-
change: PointerInputChange,
131-
): List<Item>? {
132-
val itemAtPosition = itemIndexAtPosition(change.position) ?: return null
133-
return items.filterIndexed { index, _ ->
134-
index in initialIndex..itemAtPosition || index in itemAtPosition..initialIndex
135-
}
142+
/**
143+
* Gets the index of the item that was hit by the drag.
144+
*
145+
* @param[hitPoint] The point where the drag hit the grid.
146+
* @return The index of the item that was hit by the drag, or the index of the last item if the
147+
* drag has gone past the last item.
148+
*/
149+
private fun LazyGridState.getItemPosition(hitPoint: Offset): Int? {
150+
return itemIndexAtPosition(hitPoint)
151+
?: if (isPastLastItem(hitPoint)) layoutInfo.totalItemsCount - 1 else null
136152
}
137153

138154
/**
139-
* Get the items that are overscrolled when dragging.
155+
* Determines if the drag has gone past the last item in the list.
140156
*
141-
* If the user has dragged past the last item in the list, this will return all items after the
142-
* initial index.
157+
* @param[hitPoint] The point where the drag hit the grid.
158+
* @return True if the drag has gone past the last item in the list, false otherwise.
143159
*/
144-
private fun <Item> LazyGridState.getOverscrollItems(
145-
items: List<Item>,
146-
initialIndex: Int,
147-
change: PointerInputChange,
148-
): List<Item>? {
160+
private fun LazyGridState.isPastLastItem(hitPoint: Offset): Boolean {
149161
// Get the last item in the list
150162
val lastItem = layoutInfo.visibleItemsInfo.lastOrNull()
151163
?.takeIf { it.index == layoutInfo.totalItemsCount - 1 }
152-
?: return null
164+
?: return false
153165

154166
// Determine if we have dragged past the last item in the list
155-
return if (change.position.y > lastItem.offset.y) {
156-
// If we have, return all items after the initial index
157-
items.filterIndexed { index, _ -> index >= initialIndex }
158-
} else null
167+
return hitPoint.y > lastItem.offset.y
168+
}
169+
170+
/**
171+
* Gets a list of items that are selected based on the current drag state.
172+
*
173+
* @receiver The list of items in the grid.
174+
* @param[itemPosition] The position of the item that was hit by the drag.
175+
* @param[dragState] The current drag state.
176+
* @param[isSelected] A function to determine if an item is selected.
177+
* @return A list of items that are selected based on the current drag state.
178+
*/
179+
private fun <Item> List<Item>.getSelectedItems(
180+
itemPosition: Int,
181+
dragState: DragState,
182+
isSelected: (Item) -> Boolean,
183+
): List<Item> {
184+
val (initial, current) = dragState
185+
return filterIndexed { index, item ->
186+
// Determine if the item is within the drag range
187+
val withinRange = index in initial..itemPosition || index in itemPosition..initial
188+
189+
// Determine if the item was previously selected and is still within the drag range
190+
val selected = isSelected(item)
191+
val previouslySelectedInRange =
192+
selected && index !in initial..current && index !in current..initial
193+
194+
withinRange || previouslySelectedInRange
195+
}
159196
}

demo/android/src/main/kotlin/com/dragselectcompose/demo/LazyDragSelectPhotoGrid.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fun LazyDragSelectPhotoGrid(
3838
items = photoItems,
3939
state = dragSelectState,
4040
verticalArrangement = Arrangement.spacedBy(3.dp),
41-
horizontalArrangement = Arrangement.spacedBy(3.dp)
41+
horizontalArrangement = Arrangement.spacedBy(3.dp),
4242
) {
4343
items(key = { it.id }) { photo ->
4444
SelectableItem(

demo/kmm/shared/src/commonMain/kotlin/com/dragselectcompose/demo/PhotoGrid.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ fun PhotoGrid(
2828
items = photoItems,
2929
state = dragSelectState,
3030
verticalArrangement = Arrangement.spacedBy(3.dp),
31-
horizontalArrangement = Arrangement.spacedBy(3.dp)
31+
horizontalArrangement = Arrangement.spacedBy(3.dp),
3232
) {
3333
items(key = { it.id }) { photo ->
3434
SelectableItem(

0 commit comments

Comments
 (0)