Skip to content

Commit eefd80d

Browse files
authored
Feature: Custom comparator (#30)
- Add ability to provide custom compartor for items - Save the indexes to a bundle and restore
1 parent 9bfc0f5 commit eefd80d

File tree

8 files changed

+129
-37
lines changed

8 files changed

+129
-37
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ It provides the following functionality:
137137
of the list.
138138
- Will trigger a "long-press" haptics if `enableHaptics` is `true`.
139139

140+
**Note:** By default selected items will be compared using an equality check. If your item is not
141+
a `data class` you must implement `equals` and `hashCode` for your item. Or you can pass a lambda
142+
to `rememberDragSelectState` to compare your items:
143+
144+
```kotlin
145+
val dragSelectState = rememberDragSelectState<Foo>(compareSelector = { it.someProperty })
146+
```
147+
140148
You can then use `DragSelectState` to render your list of items:
141149

142150
### Basic Example

core/api/android/core.api

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
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;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
3+
public fun <init> (Landroidx/compose/foundation/lazy/grid/LazyGridState;ILjava/util/List;)V
4+
public fun <init> (Ljava/util/List;Landroidx/compose/foundation/lazy/grid/LazyGridState;Lkotlin/jvm/functions/Function1;Lcom/dragselectcompose/core/DragState;)V
65
public final fun addSelected (Ljava/lang/Object;)V
76
public final fun clear ()V
7+
public fun equals (Ljava/lang/Object;)Z
88
public final fun getGridState ()Landroidx/compose/foundation/lazy/grid/LazyGridState;
99
public final fun getInSelectionMode ()Z
1010
public final fun getSelected ()Ljava/util/List;
11+
public fun hashCode ()I
1112
public final fun isSelected (Ljava/lang/Object;)Z
1213
public final fun removeSelected (Ljava/lang/Object;)V
1314
public final fun updateSelected (Ljava/util/List;)V
1415
}
1516

1617
public final class com/dragselectcompose/core/DragSelectStateKt {
1718
public static final fun rememberDragSelectState (Landroidx/compose/foundation/lazy/grid/LazyGridState;Ljava/util/List;Landroidx/compose/runtime/Composer;II)Lcom/dragselectcompose/core/DragSelectState;
19+
public static final fun rememberDragSelectState (Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/lazy/grid/LazyGridState;Ljava/util/List;Landroidx/compose/runtime/Composer;II)Lcom/dragselectcompose/core/DragSelectState;
20+
}
21+
22+
public final class com/dragselectcompose/core/DragState {
23+
public static final field $stable I
24+
public fun <init> (II)V
1825
}
1926

2027
public final class com/dragselectcompose/core/GridDragSelectDefaults {

core/api/desktop/core.api

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
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;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
3+
public fun <init> (Landroidx/compose/foundation/lazy/grid/LazyGridState;ILjava/util/List;)V
4+
public fun <init> (Ljava/util/List;Landroidx/compose/foundation/lazy/grid/LazyGridState;Lkotlin/jvm/functions/Function1;Lcom/dragselectcompose/core/DragState;)V
65
public final fun addSelected (Ljava/lang/Object;)V
76
public final fun clear ()V
7+
public fun equals (Ljava/lang/Object;)Z
88
public final fun getGridState ()Landroidx/compose/foundation/lazy/grid/LazyGridState;
99
public final fun getInSelectionMode ()Z
1010
public final fun getSelected ()Ljava/util/List;
11+
public fun hashCode ()I
1112
public final fun isSelected (Ljava/lang/Object;)Z
1213
public final fun removeSelected (Ljava/lang/Object;)V
1314
public final fun updateSelected (Ljava/util/List;)V
1415
}
1516

1617
public final class com/dragselectcompose/core/DragSelectStateKt {
1718
public static final fun rememberDragSelectState (Landroidx/compose/foundation/lazy/grid/LazyGridState;Ljava/util/List;Landroidx/compose/runtime/Composer;II)Lcom/dragselectcompose/core/DragSelectState;
19+
public static final fun rememberDragSelectState (Lkotlin/jvm/functions/Function1;Landroidx/compose/foundation/lazy/grid/LazyGridState;Ljava/util/List;Landroidx/compose/runtime/Composer;II)Lcom/dragselectcompose/core/DragSelectState;
20+
}
21+
22+
public final class com/dragselectcompose/core/DragState {
23+
public static final field $stable I
24+
public fun <init> (II)V
1825
}
1926

2027
public final class com/dragselectcompose/core/GridDragSelectDefaults {

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

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package com.dragselectcompose.core
33
import androidx.compose.foundation.lazy.grid.LazyGridState
44
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
55
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.Stable
67
import androidx.compose.runtime.getValue
78
import androidx.compose.runtime.mutableStateOf
89
import androidx.compose.runtime.remember
910
import androidx.compose.runtime.saveable.rememberSaveable
1011
import androidx.compose.runtime.setValue
11-
import com.dragselectcompose.core.DragState.Companion.None
1212

1313
/**
1414
* Creates a [DragSelectState] that is remembered across compositions.
@@ -25,10 +25,35 @@ import com.dragselectcompose.core.DragState.Companion.None
2525
public fun <Item> rememberDragSelectState(
2626
lazyGridState: LazyGridState = rememberLazyGridState(),
2727
initialSelection: List<Item> = emptyList(),
28+
): DragSelectState<Item> = rememberDragSelectState({ it as Any }, lazyGridState, initialSelection)
29+
30+
/**
31+
* Creates a [DragSelectState] that is remembered across compositions.
32+
*
33+
* Changes to the provided initial values will **not** result in the state being recreated or
34+
* changed in any way if it has already been created.
35+
*
36+
* @param[Item] The type of the items in the list.
37+
* @param[lazyGridState] The [LazyGridState] that will be used to control the items in the grid.
38+
* @param[initialSelection] The initial selection of items.
39+
* @param[compareSelector] A factory for selecting a property of [Item] to compare.
40+
* @return A [DragSelectState] that can be used to control the selection.
41+
*/
42+
@Composable
43+
public fun <Item> rememberDragSelectState(
44+
compareSelector: (Item) -> Any = { it as Any },
45+
lazyGridState: LazyGridState = rememberLazyGridState(),
46+
initialSelection: List<Item> = emptyList(),
2847
): DragSelectState<Item> {
29-
val indexes by rememberSaveable { mutableStateOf(None to None) }
48+
val dragState = rememberSaveable(saver = DragState.Saver) { DragState.create() }
49+
3050
return remember(lazyGridState) {
31-
DragSelectState(lazyGridState, indexes.first, initialSelection, indexes.second)
51+
DragSelectState(
52+
initialSelection = initialSelection,
53+
gridState = lazyGridState,
54+
compareSelector = compareSelector,
55+
dragState = dragState,
56+
)
3257
}
3358
}
3459

@@ -38,24 +63,34 @@ public fun <Item> rememberDragSelectState(
3863
* In most cases, this will be created via [rememberDragSelectState].
3964
*
4065
* @param[Item] The type of the items in the list.
41-
* @param[gridState] The [LazyGridState] that will be used to control the items in the grid.
42-
* @param[initialIndex] The initial index of the item that was long pressed.
4366
* @param[initialSelection] The initial selection of items.
44-
* @param[currentIndex] The current index of the item that is being dragged over.
67+
* @param[gridState] The [LazyGridState] that will be used to control the items in the grid.
68+
* @param[dragState] The current drag state.
69+
* @param[compareSelector] A factory for selecting a property of [Item] to compare.
4570
*/
71+
@Stable
4672
public class DragSelectState<Item>(
47-
public val gridState: LazyGridState,
48-
initialIndex: Int,
4973
initialSelection: List<Item>,
50-
currentIndex: Int = initialIndex,
74+
public val gridState: LazyGridState,
75+
internal val compareSelector: (Item) -> Any,
76+
internal var dragState: DragState,
5177
) {
5278

79+
/**
80+
* A state object that can be hoisted to control and observe selected items.
81+
*
82+
* In most cases, this will be created via [rememberDragSelectState].
83+
*
84+
* @param[Item] The type of the items in the list.
85+
* @param[gridState] The [LazyGridState] that will be used to control the items in the grid.
86+
* @param[initialIndex] The initial index of the item that was long pressed.
87+
* @param[initialSelection] The initial selection of items.
88+
*/
5389
public constructor(
5490
gridState: LazyGridState,
91+
initialIndex: Int,
5592
initialSelection: List<Item>,
56-
) : this(gridState, None, initialSelection, None)
57-
58-
private var dragState: DragState = DragState(initialIndex, currentIndex)
93+
) : this(initialSelection, gridState, { it as Any }, DragState.create(initial = initialIndex))
5994

6095
/**
6196
* The state containing the selected items.
@@ -65,19 +100,22 @@ public class DragSelectState<Item>(
65100
/**
66101
* The currently selected items.
67102
*/
103+
@Stable
68104
public val selected: List<Item>
69105
get() = selectedState
70106

71107
/**
72108
* Whether or not the grid is in selection mode.
73109
*/
110+
@Stable
74111
public val inSelectionMode: Boolean
75112
get() = selectedState.isNotEmpty()
76113

77114
internal val autoScrollSpeed = mutableStateOf(0f)
78115

79116
/**
80-
* Will only invoke [block] if the initial index is not [None]. Meaning we are in selection mode.
117+
* Will only invoke [block] if the initial index is not [DragState.None].
118+
* Meaning we are in selection mode.
81119
*/
82120
internal fun whenDragging(
83121
block: DragSelectState<Item>.(dragState: DragState) -> Unit,
@@ -88,11 +126,12 @@ public class DragSelectState<Item>(
88126
}
89127

90128
internal fun updateDrag(current: Int) {
91-
dragState = dragState.copy(current = current)
129+
dragState.current = current
92130
}
93131

94132
internal fun startDrag(item: Item, index: Int) {
95-
dragState = DragState(index, index)
133+
dragState.initial = index
134+
dragState.current = index
96135
addSelected(item)
97136
}
98137

@@ -102,7 +141,8 @@ public class DragSelectState<Item>(
102141
* @param[item] The item to check.
103142
* @return Whether or not the item is selected.
104143
*/
105-
public fun isSelected(item: Item): Boolean = selectedState.contains(item)
144+
public fun isSelected(item: Item): Boolean = selectedState
145+
.find { compareSelector(it) == compareSelector(item) } != null
106146

107147
/**
108148
* Updates the selected items.
@@ -127,8 +167,8 @@ public class DragSelectState<Item>(
127167
*
128168
* @param[item] The item to remove.
129169
*/
130-
public fun removeSelected(photo: Item) {
131-
selectedState -= photo
170+
public fun removeSelected(item: Item) {
171+
selectedState -= item
132172
}
133173

134174
/**
@@ -142,7 +182,27 @@ public class DragSelectState<Item>(
142182
* Resets the drag state.
143183
*/
144184
internal fun stopDrag() {
145-
dragState = dragState.copy(initial = DragState.None)
185+
dragState.initial = DragState.None
146186
autoScrollSpeed.value = 0f
147187
}
188+
189+
override fun equals(other: Any?): Boolean {
190+
if (this === other) return true
191+
if (other == null || this::class != other::class) return false
192+
193+
other as DragSelectState<*>
194+
195+
if (gridState != other.gridState) return false
196+
if (compareSelector != other.compareSelector) return false
197+
if (dragState != other.dragState) return false
198+
return autoScrollSpeed == other.autoScrollSpeed
199+
}
200+
201+
override fun hashCode(): Int {
202+
var result = gridState.hashCode()
203+
result = 31 * result + compareSelector.hashCode()
204+
result = 31 * result + dragState.hashCode()
205+
result = 31 * result + autoScrollSpeed.hashCode()
206+
return result
207+
}
148208
}
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package com.dragselectcompose.core
22

3+
import androidx.compose.runtime.saveable.Saver
4+
35
/**
46
* Represents the current state of a drag gesture.
57
*
68
* @param[initial] The index of the item where the drag gesture started.
79
* @param[current] The index of the item where the drag gesture is currently at.
810
*/
9-
internal data class DragState(
10-
val initial: Int,
11-
val current: Int,
11+
public class DragState(
12+
internal var initial: Int,
13+
internal var current: Int,
1214
) {
1315

1416
internal val isDragging: Boolean
@@ -17,5 +19,15 @@ internal data class DragState(
1719
internal companion object {
1820

1921
internal const val None = -1
22+
23+
internal fun create(
24+
initial: Int = None,
25+
current: Int = None,
26+
): DragState = DragState(initial, current)
27+
28+
internal val Saver = Saver<DragState, Pair<Int, Int>>(
29+
save = { it.initial to it.current },
30+
restore = { (initial, current) -> create(initial = initial, current = current) }
31+
)
2032
}
2133
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,6 @@ 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-
7874
pointerInput(Unit) {
7975
detectDragGesturesAfterLongPress(
8076
onDragStart = { offset ->
@@ -95,7 +91,9 @@ public fun <Item> Modifier.gridDragSelect(
9591
val itemPosition = gridState.getItemPosition(change.position)
9692
?: return@whenDragging
9793

98-
val newSelection = items.getSelectedItems(itemPosition, dragState, isSelected)
94+
val newSelection =
95+
items.getSelectedItems(itemPosition, dragState, state::isSelected)
96+
9997
updateDrag(current = itemPosition)
10098
updateSelected(newSelection)
10199
}
@@ -181,7 +179,7 @@ private fun <Item> List<Item>.getSelectedItems(
181179
dragState: DragState,
182180
isSelected: (Item) -> Boolean,
183181
): List<Item> {
184-
val (initial, current) = dragState
182+
val (initial, current) = dragState.run { initial to current }
185183
return filterIndexed { index, item ->
186184
// Determine if the item is within the drag range
187185
val withinRange = index in initial..itemPosition || index in itemPosition..initial

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import com.dragselectcompose.grid.indicator.UnselectedIcon
3030
fun LazyDragSelectPhotoGrid(
3131
modifier: Modifier = Modifier,
3232
photoItems: List<PhotoItem> = PhotoItem.createList(100),
33-
dragSelectState: DragSelectState<PhotoItem> = rememberDragSelectState(),
33+
dragSelectState: DragSelectState<PhotoItem> = rememberDragSelectState(compareSelector = { it.id }),
3434
) {
3535
LazyDragSelectVerticalGrid(
3636
modifier = modifier,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import io.ktor.http.Url
2020
fun PhotoGrid(
2121
modifier: Modifier = Modifier,
2222
photoItems: List<PhotoItem> = PhotoItem.createList(100),
23-
dragSelectState: DragSelectState<PhotoItem> = rememberDragSelectState(),
23+
dragSelectState: DragSelectState<PhotoItem> = rememberDragSelectState(compareSelector = { it.id }),
2424
) {
2525
LazyDragSelectVerticalGrid(
2626
modifier = modifier,
@@ -30,7 +30,7 @@ fun PhotoGrid(
3030
verticalArrangement = Arrangement.spacedBy(3.dp),
3131
horizontalArrangement = Arrangement.spacedBy(3.dp),
3232
) {
33-
items(key = { it.id }) { photo ->
33+
items { photo ->
3434
SelectableItem(
3535
item = photo,
3636
selectedIcon = { SelectedIcon(Modifier.align(Alignment.BottomEnd)) },

0 commit comments

Comments
 (0)