Skip to content

Commit fb7d2bd

Browse files
authored
Merge pull request #62 from MohamedRejeb/0.5.x
Docs improvements
2 parents ed71475 + feb85e5 commit fb7d2bd

11 files changed

Lines changed: 130 additions & 15 deletions

File tree

compose-dnd/src/commonMain/kotlin/com/mohamedrejeb/compose/dnd/drag/DraggableItemModifier.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ fun <T> Modifier.draggableItem(
9292
sizeDropAnimationSpec = sizeDropAnimationSpec,
9393
draggableContent = draggableContent,
9494
)
95-
)
96-
.pointerInput(
95+
).pointerInput(
9796
key,
9897
enabled,
9998
state,

compose-dnd/src/commonMain/kotlin/com/mohamedrejeb/compose/dnd/drag/DropStrategy.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,35 @@ import androidx.compose.ui.geometry.Size
2020
import com.mohamedrejeb.compose.dnd.drop.DropTargetState
2121
import com.mohamedrejeb.compose.dnd.utils.MathUtils
2222

23+
/**
24+
* Strategy for determining which drop target receives the dragged item when
25+
* multiple targets overlap.
26+
*
27+
* Built-in strategies:
28+
* - [Surface] — Largest absolute overlap area wins.
29+
* - [SurfacePercentage] — Largest overlap as a percentage of the dragged item wins. (Default)
30+
* - [CenterDistance] — Closest center-to-center distance wins.
31+
*/
2332
interface DropStrategy {
2433

34+
/**
35+
* Determine which drop target the dragged item is currently hovering over.
36+
*
37+
* @param draggedItemTopLeft Top-left position of the dragged item in root coordinates.
38+
* @param draggedItemSize Size of the dragged item.
39+
* @param dropTargets List of candidate drop targets that intersect with the dragged item.
40+
* @return The drop target that should receive the drop, or null if none qualifies.
41+
*/
2542
fun <T> getHoveredDropTarget(
2643
draggedItemTopLeft: Offset,
2744
draggedItemSize: Size,
2845
dropTargets: List<DropTargetState<T>>,
2946
): DropTargetState<T>?
3047

48+
/**
49+
* Selects the target with the largest absolute overlap area.
50+
* Z-index is factored in by adding `zIndex * maxArea` to the score.
51+
*/
3152
object Surface : DropStrategy {
3253
override fun <T> getHoveredDropTarget(
3354
draggedItemTopLeft: Offset,
@@ -47,6 +68,10 @@ interface DropStrategy {
4768
}
4869
}
4970

71+
/**
72+
* Selects the target with the largest overlap as a percentage of the dragged item area.
73+
* This is the default strategy. Z-index is added to the percentage score.
74+
*/
5075
object SurfacePercentage : DropStrategy {
5176
override fun <T> getHoveredDropTarget(
5277
draggedItemTopLeft: Offset,
@@ -66,6 +91,9 @@ interface DropStrategy {
6691
}
6792
}
6893

94+
/**
95+
* Selects the target whose center is closest to the dragged item's center.
96+
*/
6997
object CenterDistance : DropStrategy {
7098
override fun <T> getHoveredDropTarget(
7199
draggedItemTopLeft: Offset,

compose-dnd/src/commonMain/kotlin/com/mohamedrejeb/compose/dnd/drop/DropTarget.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,18 @@ import com.mohamedrejeb.compose.dnd.drag.DraggedItemState
3636
* Mark this composable as a drop target.
3737
*
3838
* @param key The key used to identify the drop target.
39-
* @param zIndex The z-index of the drop target.
4039
* @param state The drag and drop state.
41-
* @param dropAlignment The alignment of the dropped item.
42-
* @param dropOffset The offset of the dropped item.
40+
* @param zIndex The z-index of the drop target. When multiple targets overlap,
41+
* the one with the highest z-index takes priority.
42+
* @param dropAlignment The alignment of the dropped item within this target
43+
* for the drop animation.
44+
* @param dropOffset An additional offset applied to the drop animation position.
45+
* @param dropAnimationEnabled Whether the drop animation is enabled. When false,
46+
* the dragged item disappears immediately on drop without animating to the target.
47+
* @param canDrop Whether this target currently accepts drops. When false, the target
48+
* is excluded from hover detection — dragged items cannot hover over it or be dropped on it.
49+
* This is a Compose-evaluated boolean, so it can read state (e.g., `state.draggedItem?.data`)
50+
* to dynamically accept or reject specific items.
4351
* @param onDrop The action to perform when an item is dropped onto the target.
4452
* Accepts the dragged item state as a parameter.
4553
* @param onDragEnter The action to perform when an item is dragged over the target.

compose-dnd/src/commonMain/kotlin/com/mohamedrejeb/compose/dnd/reorder/ReorderState.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ import com.mohamedrejeb.compose.dnd.annotation.ExperimentalDndApi
2323

2424
/**
2525
* Remember [ReorderState]
26-
* @param dragAfterLongPress if true, drag will start after long press, otherwise drag will start after simple press
26+
* @param dragAfterLongPress if true, drag will start after long press, otherwise drag will start after simple press.
2727
* This parameter is applied to all [ReorderableItem]s. If you want to change it for a specific item, use [ReorderableItem] parameter.
28+
* @param requireFirstDownUnconsumed if true, the first down event must be unconsumed to start the drag.
2829
* @return [ReorderState]
2930
*/
3031
@Composable

compose-dnd/src/commonMain/kotlin/com/mohamedrejeb/compose/dnd/reorder/ReorderableItemModifier.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,7 @@ fun <T> Modifier.reorderableItem(
9898
dropAnimationSpec = dropAnimationSpec,
9999
sizeDropAnimationSpec = sizeDropAnimationSpec,
100100
draggableContent = draggableContent,
101-
)
102-
.dropTarget(
101+
).dropTarget(
103102
key = key,
104103
state = state,
105104
zIndex = zIndex,

compose-dnd/src/commonMain/kotlin/com/mohamedrejeb/compose/dnd/reorder/ReorderableItemScope.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import com.mohamedrejeb.compose.dnd.drag.DragHandleModifier
2424
import com.mohamedrejeb.compose.dnd.drag.DraggableItemScope
2525
import com.mohamedrejeb.compose.dnd.drag.DraggableItemState
2626

27+
/**
28+
* Scope for [ReorderableItem] content. Extends [DraggableItemScope] with
29+
* access to [isDragging] state and [dragHandle] modifier.
30+
*/
2731
interface ReorderableItemScope : DraggableItemScope
2832

2933
internal class ReorderableItemScopeImpl<T>(

docs/drag-and-drop/overview.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Compose DND provides a declarative API for adding drag and drop functionality to
44

55
- `DragAndDropState` -- Holds the state for all drag and drop operations.
66
- `DragAndDropContainer` -- A container that wraps all draggable items and drop targets.
7-
- `DraggableItem` -- Makes a composable draggable.
7+
- `DraggableItem` composable -- Makes a composable draggable (wrapper approach).
8+
- `draggableItem` modifier -- Makes a composable draggable (modifier approach, less boilerplate).
89
- `dropTarget` modifier -- Marks a composable as a drop target.
910

1011
## Creating DragAndDropState
@@ -91,6 +92,32 @@ Inside `DraggableItem`'s content lambda, you have access to `DraggableItemScope`
9192
- `key: Any` -- The key of this item.
9293
- `isDragging: Boolean` -- Whether this item is currently being dragged.
9394

95+
## draggableItem Modifier
96+
97+
As an alternative to the `DraggableItem` composable wrapper, you can use the `Modifier.draggableItem` modifier directly. This reduces boilerplate by eliminating the wrapper composable.
98+
99+
```kotlin
100+
val isDragging = dragAndDropState.isDragging("item-1")
101+
102+
Text(
103+
text = "Drag me",
104+
modifier = Modifier
105+
.graphicsLayer { alpha = if (isDragging) 0f else 1f }
106+
.draggableItem(
107+
state = dragAndDropState,
108+
key = "item-1",
109+
data = "Hello World",
110+
draggableContent = {
111+
Text("Drag me") // Content shown as drag shadow
112+
},
113+
)
114+
)
115+
```
116+
117+
The `draggableItem` modifier accepts the same parameters as `DraggableItem` (except `content`, which is the composable it's applied to). The `draggableContent` parameter is required — it defines what the drag shadow looks like.
118+
119+
Use `DragAndDropState.isDragging(key)` to check if a specific item is being dragged.
120+
94121
## Drop Target
95122

96123
Use the `Modifier.dropTarget` extension to mark any composable as a drop target.
@@ -127,6 +154,7 @@ Box(
127154
| `dropAlignment` | `Alignment` | `Alignment.Center` | Alignment of the dropped item within the target for the drop animation. |
128155
| `dropOffset` | `Offset` | `Offset.Zero` | Additional offset for the drop animation position. |
129156
| `dropAnimationEnabled`| `Boolean` | `true` | Whether to animate the drop. If `false`, the drop callback fires immediately. |
157+
| `canDrop` | `Boolean` | `true` | Whether this target accepts drops. When `false`, dragged items cannot hover over or be dropped on this target. Can read `state.draggedItem?.data` to validate dynamically. |
130158
| `onDrop` | `(DraggedItemState<T>) -> Unit` | `{}` | Called when an item is dropped on this target. |
131159
| `onDragEnter` | `(DraggedItemState<T>) -> Unit` | `{}` | Called when a dragged item enters this target. |
132160
| `onDragExit` | `(DraggedItemState<T>) -> Unit` | `{}` | Called when a dragged item exits this target. |

docs/drag-and-drop/reorder.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,48 @@ private fun ItemCard(
204204

205205
!!! tip
206206
Use `graphicsLayer { alpha = if (isDragging) 0f else 1f }` to hide the original item while it is being dragged, so only the drag shadow is visible.
207+
208+
## reorderableItem Modifier
209+
210+
As an alternative to the `ReorderableItem` composable wrapper, you can use the `Modifier.reorderableItem` modifier. This combines `draggableItem` and `dropTarget` into a single modifier and reduces boilerplate.
211+
212+
```kotlin
213+
val dndState = rememberDragAndDropState<String>()
214+
215+
DragAndDropContainer(state = dndState) {
216+
LazyColumn(
217+
verticalArrangement = Arrangement.spacedBy(12.dp),
218+
) {
219+
items(items, key = { it }) { item ->
220+
val isDragging = dndState.isDragging(item)
221+
222+
ItemCard(
223+
text = item,
224+
modifier = Modifier
225+
.graphicsLayer { alpha = if (isDragging) 0f else 1f }
226+
.reorderableItem(
227+
key = item,
228+
data = item,
229+
state = dndState,
230+
onDragEnter = { state ->
231+
items = items.toMutableList().apply {
232+
val index = indexOf(item)
233+
if (index != -1) {
234+
remove(state.data)
235+
add(index, state.data)
236+
}
237+
}
238+
},
239+
draggableContent = {
240+
ItemCard(text = item, isDragShadow = true)
241+
},
242+
)
243+
.fillMaxWidth(),
244+
)
245+
}
246+
}
247+
}
248+
```
249+
250+
!!! note
251+
When using the modifier API, use `rememberDragAndDropState` and `DragAndDropContainer` directly instead of `rememberReorderState` and `ReorderContainer`. Use `DragAndDropState.isDragging(key)` to check drag state.

sample/common/src/commonMain/kotlin/ui/ConditionalDropScreen.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ private fun ConditionalDropContent(
7373
val evenFull = evenTargetCount >= 2
7474
val oddFull = oddTargetCount >= 2
7575

76+
// Read the dragged item's data to validate parity in canDrop
77+
val draggedNumber = state.draggedItem?.data?.toIntOrNull()
78+
7679
DragAndDropContainer(
7780
state = state,
7881
modifier = modifier,
@@ -130,7 +133,8 @@ private fun ConditionalDropContent(
130133
.dropTarget(
131134
key = "even",
132135
state = state,
133-
canDrop = !evenFull,
136+
canDrop = !evenFull && (draggedNumber == null || draggedNumber % 2 == 0),
137+
dropAnimationEnabled = false,
134138
onDrop = { droppedItem ->
135139
val num = droppedItem.data.toIntOrNull() ?: return@dropTarget
136140
if (num % 2 == 0) {
@@ -152,7 +156,8 @@ private fun ConditionalDropContent(
152156
.dropTarget(
153157
key = "odd",
154158
state = state,
155-
canDrop = !oddFull,
159+
canDrop = !oddFull && (draggedNumber == null || draggedNumber % 2 != 0),
160+
dropAnimationEnabled = false,
156161
onDrop = { droppedItem ->
157162
val num = droppedItem.data.toIntOrNull() ?: return@dropTarget
158163
if (num % 2 != 0) {

sample/common/src/commonMain/kotlin/ui/ItemToItemOneDirectionScreen.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ private fun ItemToItemOneDirectionScreenContent(
126126
draggableContent = {
127127
RedBox(modifier = Modifier.size(200.dp))
128128
},
129-
)
130-
.size(200.dp),
129+
).size(200.dp),
131130
)
132131
}
133132
}

0 commit comments

Comments
 (0)