Skip to content

Commit e986fee

Browse files
authored
Merge pull request #60 from MohamedRejeb/0.5.x
feat: add `draggableItem` modifier for easier drag-and-drop integration
2 parents 244bf56 + 6c158d8 commit e986fee

2 files changed

Lines changed: 265 additions & 13 deletions

File tree

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Copyright 2023, Mohamed Ben Rejeb and the Compose Dnd project contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.mohamedrejeb.compose.dnd.drag
17+
18+
import androidx.compose.animation.core.AnimationSpec
19+
import androidx.compose.animation.core.SpringSpec
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.geometry.Offset
23+
import androidx.compose.ui.geometry.Size
24+
import androidx.compose.ui.input.pointer.pointerInput
25+
import androidx.compose.ui.layout.LayoutCoordinates
26+
import androidx.compose.ui.layout.positionInRoot
27+
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
28+
import androidx.compose.ui.node.LayoutAwareModifierNode
29+
import androidx.compose.ui.node.ModifierNodeElement
30+
import androidx.compose.ui.node.currentValueOf
31+
import androidx.compose.ui.platform.InspectorInfo
32+
import androidx.compose.ui.unit.IntSize
33+
import androidx.compose.ui.unit.toSize
34+
import com.mohamedrejeb.compose.dnd.DragAndDropState
35+
import com.mohamedrejeb.compose.dnd.LocalDragAndDropInfo
36+
import com.mohamedrejeb.compose.dnd.gesture.detectDragStartGesture
37+
38+
/**
39+
* Mark this composable as a draggable item.
40+
*
41+
* This is a modifier-based alternative to [DraggableItem] that reduces boilerplate.
42+
* Apply it to any composable to make it draggable.
43+
*
44+
* To check if the item is currently being dragged, use [DragAndDropState.isDragging]:
45+
* ```
46+
* val isDragging = state.isDragging(key)
47+
* ```
48+
*
49+
* @param key Unique key for this draggable item.
50+
* @param data Data passed to the drop target on drop.
51+
* @param state The drag and drop state.
52+
* @param enabled Whether drag is enabled for this item.
53+
* @param dragAfterLongPress If true, drag starts after long press; otherwise after press + slop.
54+
* @param requireFirstDownUnconsumed If true, first down event must be unconsumed.
55+
* @param dropTargets List of drop target keys this item can be dropped on. Empty = any target.
56+
* @param dropStrategy Strategy to determine which drop target receives the item.
57+
* @param dragAxis Constrain movement to [DragAxis.Free], [DragAxis.Horizontal], or [DragAxis.Vertical].
58+
* @param hasDragHandle If true, drag is initiated from a [dragHandle] modifier instead of the whole item.
59+
* @param dropAnimationSpec Animation spec for position on drop.
60+
* @param sizeDropAnimationSpec Animation spec for size on drop.
61+
* @param draggableContent Content rendered as the drag shadow/overlay while dragging.
62+
*/
63+
fun <T> Modifier.draggableItem(
64+
key: Any,
65+
data: T,
66+
state: DragAndDropState<T>,
67+
enabled: Boolean = true,
68+
dragAfterLongPress: Boolean = state.dragAfterLongPress,
69+
requireFirstDownUnconsumed: Boolean = state.requireFirstDownUnconsumed,
70+
dropTargets: List<Any> = emptyList(),
71+
dropStrategy: DropStrategy = DropStrategy.SurfacePercentage,
72+
dragAxis: DragAxis = DragAxis.Free,
73+
hasDragHandle: Boolean = false,
74+
dropAnimationSpec: AnimationSpec<Offset> = SpringSpec(),
75+
sizeDropAnimationSpec: AnimationSpec<Size> = SpringSpec(),
76+
draggableContent: @Composable () -> Unit,
77+
): Modifier =
78+
this
79+
.then(
80+
DraggableItemNodeElement(
81+
key = key,
82+
data = data,
83+
state = state,
84+
enabled = enabled,
85+
dragAfterLongPress = dragAfterLongPress,
86+
requireFirstDownUnconsumed = requireFirstDownUnconsumed,
87+
dropTargets = dropTargets,
88+
dropStrategy = dropStrategy,
89+
dragAxis = dragAxis,
90+
hasDragHandle = hasDragHandle,
91+
dropAnimationSpec = dropAnimationSpec,
92+
sizeDropAnimationSpec = sizeDropAnimationSpec,
93+
draggableContent = draggableContent,
94+
)
95+
)
96+
.pointerInput(
97+
key,
98+
enabled,
99+
state,
100+
state.enabled,
101+
dragAfterLongPress,
102+
requireFirstDownUnconsumed,
103+
) {
104+
if (!enabled || !state.enabled) return@pointerInput
105+
val draggableItemState = state.draggableItemMap[key] ?: return@pointerInput
106+
detectDragStartGesture(
107+
key = key,
108+
state = state,
109+
draggableItemState = draggableItemState,
110+
enabled = true,
111+
dragAfterLongPress = dragAfterLongPress,
112+
requireFirstDownUnconsumed = requireFirstDownUnconsumed,
113+
)
114+
}
115+
116+
/**
117+
* Returns true if an item with the given [key] is currently being dragged.
118+
*/
119+
fun <T> DragAndDropState<T>.isDragging(key: Any): Boolean =
120+
draggedItem?.key == key
121+
122+
// -- Internal implementation --
123+
124+
private data class DraggableItemNodeElement<T>(
125+
val key: Any,
126+
val data: T,
127+
val state: DragAndDropState<T>,
128+
val enabled: Boolean,
129+
val dragAfterLongPress: Boolean,
130+
val requireFirstDownUnconsumed: Boolean,
131+
val dropTargets: List<Any>,
132+
val dropStrategy: DropStrategy,
133+
val dragAxis: DragAxis,
134+
val hasDragHandle: Boolean,
135+
val dropAnimationSpec: AnimationSpec<Offset>,
136+
val sizeDropAnimationSpec: AnimationSpec<Size>,
137+
val draggableContent: @Composable () -> Unit,
138+
) : ModifierNodeElement<DraggableItemNode<T>>() {
139+
140+
override fun create(): DraggableItemNode<T> =
141+
DraggableItemNode(
142+
draggableItemState = DraggableItemState(
143+
key = key,
144+
data = data,
145+
positionInRoot = Offset.Zero,
146+
size = Size.Zero,
147+
dropTargets = dropTargets,
148+
dropStrategy = dropStrategy,
149+
dragAxis = dragAxis,
150+
hasDragHandle = hasDragHandle,
151+
dropAnimationSpec = dropAnimationSpec,
152+
sizeDropAnimationSpec = sizeDropAnimationSpec,
153+
content = draggableContent,
154+
),
155+
state = state,
156+
enabled = enabled,
157+
dragAfterLongPress = dragAfterLongPress,
158+
requireFirstDownUnconsumed = requireFirstDownUnconsumed,
159+
)
160+
161+
override fun update(node: DraggableItemNode<T>) {
162+
node.apply {
163+
val isKeyChanged = draggableItemState.key != key
164+
165+
this.state = state
166+
this.enabled = enabled
167+
this.dragAfterLongPress = dragAfterLongPress
168+
this.requireFirstDownUnconsumed = requireFirstDownUnconsumed
169+
170+
draggableItemState.key = key
171+
draggableItemState.data = data
172+
draggableItemState.dropTargets = dropTargets
173+
draggableItemState.dropStrategy = dropStrategy
174+
draggableItemState.dragAxis = dragAxis
175+
draggableItemState.hasDragHandle = hasDragHandle
176+
draggableItemState.dropAnimationSpec = dropAnimationSpec
177+
draggableItemState.sizeDropAnimationSpec = sizeDropAnimationSpec
178+
draggableItemState.content = draggableContent
179+
180+
if (isKeyChanged) {
181+
onDetach()
182+
onAttach()
183+
}
184+
}
185+
}
186+
187+
override fun InspectorInfo.inspectableProperties() {
188+
name = "draggableItem"
189+
properties["key"] = key
190+
properties["state"] = state
191+
properties["enabled"] = enabled
192+
properties["dragAfterLongPress"] = dragAfterLongPress
193+
properties["dropTargets"] = dropTargets
194+
properties["dropStrategy"] = dropStrategy
195+
properties["dragAxis"] = dragAxis
196+
properties["hasDragHandle"] = hasDragHandle
197+
}
198+
}
199+
200+
private class DraggableItemNode<T>(
201+
val draggableItemState: DraggableItemState<T>,
202+
var state: DragAndDropState<T>,
203+
var enabled: Boolean,
204+
var dragAfterLongPress: Boolean,
205+
var requireFirstDownUnconsumed: Boolean,
206+
) : Modifier.Node(),
207+
LayoutAwareModifierNode,
208+
CompositionLocalConsumerModifierNode {
209+
210+
private val key get() = draggableItemState.key
211+
212+
private var isShadow = false
213+
214+
override fun onAttach() {
215+
isShadow = currentValueOf(LocalDragAndDropInfo).isShadow
216+
217+
if (isShadow) return
218+
219+
state.addDraggableItem(draggableItemState)
220+
}
221+
222+
override fun onPlaced(coordinates: LayoutCoordinates) {
223+
if (isShadow) return
224+
225+
state.addDraggableItem(draggableItemState)
226+
227+
draggableItemState.positionInRoot = coordinates.positionInRoot()
228+
draggableItemState.size = coordinates.size.toSize()
229+
}
230+
231+
override fun onRemeasured(size: IntSize) {
232+
if (isShadow) return
233+
234+
draggableItemState.size = size.toSize()
235+
}
236+
237+
override fun onReset() {
238+
if (isShadow) return
239+
240+
state.removeDraggableItem(key)
241+
}
242+
243+
override fun onDetach() {
244+
if (isShadow) return
245+
246+
state.removeDraggableItem(key)
247+
}
248+
}

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

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ import androidx.compose.ui.Modifier
3636
import androidx.compose.ui.graphics.graphicsLayer
3737
import androidx.compose.ui.unit.dp
3838
import com.mohamedrejeb.compose.dnd.DragAndDropContainer
39-
import com.mohamedrejeb.compose.dnd.drag.DraggableItem
39+
import com.mohamedrejeb.compose.dnd.drag.draggableItem
40+
import com.mohamedrejeb.compose.dnd.drag.isDragging
4041
import com.mohamedrejeb.compose.dnd.drop.dropTarget
4142
import com.mohamedrejeb.compose.dnd.rememberDragAndDropState
4243
import components.DemoScreenScaffold
@@ -113,18 +114,21 @@ private fun ItemToItemOneDirectionScreenContent(
113114
.weight(1f),
114115
) {
115116
if (!isDropped) {
116-
DraggableItem(
117-
state = dragAndDropState,
118-
key = 1,
119-
data = 1,
120-
) {
121-
RedBox(
122-
modifier = Modifier
123-
.graphicsLayer {
124-
alpha = if (isDragging) 0f else 1f
125-
}.size(200.dp),
126-
)
127-
}
117+
val isDragging = dragAndDropState.isDragging(1)
118+
119+
RedBox(
120+
modifier = Modifier
121+
.graphicsLayer { alpha = if (isDragging) 0f else 1f }
122+
.draggableItem(
123+
state = dragAndDropState,
124+
key = 1,
125+
data = 1,
126+
draggableContent = {
127+
RedBox(modifier = Modifier.size(200.dp))
128+
},
129+
)
130+
.size(200.dp),
131+
)
128132
}
129133
}
130134

0 commit comments

Comments
 (0)