Merge "Update detectDragGestures to be re-used in Draggables." into androidx-main
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index aaf6abe..44e9b90 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -374,7 +374,8 @@
) : DragGestureNode(
canDrag = AlwaysDrag,
enabled = enabled,
- interactionSource = interactionSource
+ interactionSource = interactionSource,
+ orientationLock = orientation
) {
open suspend fun AnchoredDragScope.anchoredDrag(
@@ -389,9 +390,6 @@
state.anchoredDrag(MutatePriority.Default) { anchoredDrag(forEachDelta) }
}
- override val pointerDirectionConfig: PointerDirectionConfig
- get() = orientation.toPointerDirectionConfig()
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -429,7 +427,8 @@
update(
enabled = enabled,
interactionSource = interactionSource,
- isResetPointerInputHandling = resetPointerInputHandling,
+ shouldResetPointerInputHandling = resetPointerInputHandling,
+ orientationLock = orientation
)
}
@@ -742,7 +741,8 @@
@Deprecated(
message = "Use the progress function to query the progress between two specified " +
"anchors.",
- replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)"))
+ replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)")
+ )
@get:FloatRange(from = 0.0, to = 1.0)
val progress: Float by derivedStateOf(structuralEqualityPolicy()) {
val a = anchors.positionOf(settledValue)
@@ -1296,7 +1296,7 @@
}
}
-private fun<K> ObjectFloatMap<K>.minValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.minValueOrNaN(): Float {
if (size == 1) return Float.NaN
var minValue = Float.POSITIVE_INFINITY
forEachValue { value ->
@@ -1307,7 +1307,7 @@
return minValue
}
-private fun<K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
if (size == 1) return Float.NaN
var maxValue = Float.NEGATIVE_INFINITY
forEachValue { value ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 4d7c7fc..86835e4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -45,7 +45,7 @@
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
-import kotlin.math.abs
+import kotlin.math.absoluteValue
import kotlin.math.sign
import kotlinx.coroutines.CancellationException
@@ -79,7 +79,7 @@
pointerId,
PointerType.Touch,
onPointerSlopReached = onTouchSlopReached,
- pointerDirectionConfig = BidirectionalPointerDirectionConfig,
+ orientation = null,
)
}
@@ -170,33 +170,113 @@
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
+) = detectDragGestures(
+ onDragStart = { _, offset -> onDragStart(offset) },
+ onDragEnd = { onDragEnd.invoke() },
+ onDragCancel = onDragCancel,
+ shouldAwaitTouchSlop = { true },
+ orientationLock = null,
+ onDrag = onDrag
+)
+
+/**
+ * A Gesture detector that waits for pointer down and touch slop in the direction specified by
+ * [orientationLock] and then calls [onDrag] for each drag event.
+ * It follows the touch slop detection of [awaitTouchSlopOrCancellation] but will consume the
+ * position change automatically once the touch slop has been crossed, the amount of drag over
+ * the touch slop is reported as the first drag event [onDrag] after the slop is crossed.
+ * If [shouldAwaitTouchSlop] returns true the touch slop recognition phase will be ignored
+ * and the drag gesture will be recognized immediately.The first [onDrag] in this case will report
+ * an [Offset.Zero].
+ *
+ * [onDragStart] is called when the touch slop has been passed and includes an [Offset] representing
+ * the last known pointer position relative to the containing element as well as the initial
+ * down event that triggered this gesture detection cycle. The [Offset] can be outside
+ * the actual bounds of the element itself meaning the numbers can be negative or larger than the
+ * element bounds if the touch target is smaller than the
+ * [ViewConfiguration.minimumTouchTargetSize].
+ *
+ * [onDragEnd] is called after all pointers are up with the event change of the up event
+ * and [onDragCancel] is called if another gesture has consumed pointer input,
+ * canceling this gesture.
+ *
+ * @param onDragStart A lambda to be called when the drag gesture starts, it contains information
+ * about the triggering [PointerInputChange] and post slop delta.
+ * @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
+ * up [PointerInputChange] that finished the gesture.
+ * @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
+ * when it was consumed.
+ * @param shouldAwaitTouchSlop Indicates if touch slop detection should be skipped.
+ * @param orientationLock Optionally locks detection to this orientation, this means, when this is
+ * provided, touch slop detection and drag event detection will be conditioned to the given
+ * orientation axis. [onDrag] will still dispatch events on with information in both axis, but
+ * if orientation lock is provided, only events that happen on the given orientation will be
+ * considered. If no value is provided (i.e. null) touch slop and drag detection will happen on
+ * an "any" orientation basis, that is, touch slop will be detected if crossed in either direction
+ * and drag events will be dispatched if present in either direction.
+ * @param onDrag A lambda to be called for each delta event in the gesture. It contains information
+ * about the [PointerInputChange] and the movement offset.
+ *
+ * Example Usage:
+ * @sample androidx.compose.foundation.samples.DetectDragGesturesSample
+ *
+ * @see detectVerticalDragGestures
+ * @see detectHorizontalDragGestures
+ * @see detectDragGesturesAfterLongPress to detect gestures after long press
+ */
+internal suspend fun PointerInputScope.detectDragGestures(
+ onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
+ onDragEnd: (change: PointerInputChange) -> Unit,
+ onDragCancel: () -> Unit,
+ shouldAwaitTouchSlop: () -> Boolean,
+ orientationLock: Orientation?,
+ onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) {
awaitEachGesture {
+ val initialDown =
+ awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
+ val awaitTouchSlop = shouldAwaitTouchSlop()
+
+ if (!awaitTouchSlop) {
+ initialDown.consume()
+ }
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
var overSlop = Offset.Zero
- do {
- drag = awaitPointerSlopOrCancellation(
- down.id,
- down.type,
- pointerDirectionConfig = BidirectionalPointerDirectionConfig
- ) { change, over ->
- change.consume()
- overSlop = over
- }
- } while (drag != null && !drag.isConsumed)
- if (drag != null) {
- onDragStart.invoke(drag.position)
- onDrag(drag, overSlop)
- if (
- !drag(drag.id) {
- onDrag(it, it.positionChange())
- it.consume()
+ var initialDelta = Offset.Zero
+
+ if (awaitTouchSlop) {
+ do {
+ drag = awaitPointerSlopOrCancellation(
+ down.id,
+ down.type,
+ orientation = orientationLock
+ ) { change, over ->
+ change.consume()
+ overSlop = over
}
- ) {
+ } while (drag != null && !drag.isConsumed)
+ initialDelta = drag?.position ?: Offset.Zero
+ } else {
+ drag = initialDown
+ }
+
+ if (drag != null) {
+ onDragStart.invoke(initialDown, initialDelta)
+ onDrag(drag, overSlop)
+ val upEvent = drag(
+ pointerId = drag.id,
+ onDrag = {
+ onDrag(it, it.positionChange())
+ },
+ orientation = orientationLock,
+ motionConsumed = {
+ it.isConsumed
+ })
+ if (upEvent == null) {
onDragCancel()
} else {
- onDragEnd()
+ onDragEnd(upEvent)
}
}
}
@@ -288,7 +368,7 @@
pointerId = pointerId,
pointerType = PointerType.Touch,
onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
- pointerDirectionConfig = VerticalPointerDirectionConfig
+ orientation = Orientation.Vertical
)
internal suspend fun AwaitPointerEventScope.awaitVerticalPointerSlopOrCancellation(
@@ -299,7 +379,7 @@
pointerId = pointerId,
pointerType = pointerType,
onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
- pointerDirectionConfig = VerticalPointerDirectionConfig
+ orientation = Orientation.Vertical
)
/**
@@ -324,7 +404,7 @@
): Boolean = drag(
pointerId = pointerId,
onDrag = onDrag,
- hasDragged = { it.positionChangeIgnoreConsumed().y != 0f },
+ orientation = Orientation.Vertical,
motionConsumed = { it.isConsumed }
) != null
@@ -439,7 +519,7 @@
pointerId = pointerId,
pointerType = PointerType.Touch,
onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.x) },
- pointerDirectionConfig = HorizontalPointerDirectionConfig
+ orientation = Orientation.Horizontal
)
internal suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
@@ -450,7 +530,7 @@
pointerId = pointerId,
pointerType = pointerType,
onPointerSlopReached = { change, overSlop -> onPointerSlopReached(change, overSlop.x) },
- pointerDirectionConfig = HorizontalPointerDirectionConfig
+ orientation = Orientation.Horizontal
)
/**
@@ -472,7 +552,7 @@
): Boolean = drag(
pointerId = pointerId,
onDrag = onDrag,
- hasDragged = { it.positionChangeIgnoreConsumed().x != 0f },
+ orientation = Orientation.Horizontal,
motionConsumed = { it.isConsumed }
) != null
@@ -563,9 +643,12 @@
/**
* Continues to read drag events until all pointers are up or the drag event is canceled.
- * The initial pointer to use for driving the drag is [pointerId]. [hasDragged]
- * passes the result whether a change was detected from the drag function or not. [onDrag] is called
- * whenever the pointer moves and [hasDragged] returns non-zero.
+ * The initial pointer to use for driving the drag is [pointerId]. [onDrag] is called
+ * whenever the pointer moves. The up event is returned at the end of the drag gesture.
+ *
+ * @param pointerId The pointer where that is driving the gesture.
+ * @param onDrag Callback for every new drag event.
+ * @param motionConsumed If the PointerInputChange should be considered as consumed.
*
* @return The last pointer input event change when gesture ended with all pointers up
* and null when the gesture was canceled.
@@ -573,7 +656,7 @@
internal suspend inline fun AwaitPointerEventScope.drag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit,
- hasDragged: (PointerInputChange) -> Boolean,
+ orientation: Orientation?,
motionConsumed: (PointerInputChange) -> Boolean
): PointerInputChange? {
if (currentEvent.isPointerUp(pointerId)) {
@@ -581,7 +664,15 @@
}
var pointer = pointerId
while (true) {
- val change = awaitDragOrUp(pointer, hasDragged) ?: return null
+ val change = awaitDragOrUp(pointer) {
+ val positionChange = it.positionChangeIgnoreConsumed()
+ val motionChange = if (orientation == null) {
+ positionChange.getDistance()
+ } else {
+ if (orientation == Orientation.Vertical) positionChange.y else positionChange.x
+ }
+ motionChange != 0.0f
+ } ?: return null
if (motionConsumed(change)) {
return null
@@ -629,16 +720,14 @@
}
/**
- * Waits for drag motion along one axis when [pointerDirectionConfig] is
- * [HorizontalPointerDirectionConfig] or [VerticalPointerDirectionConfig], and drag motion along
- * any axis when using [BidirectionalPointerDirectionConfig]. It passes [pointerId] as the pointer
- * to examine. If [pointerId] is raised, another pointer from those that are down will be chosen to
+ * Waits for drag motion and uses [orientation] to detect the direction of touch slop detection.
+ * It passes [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer from
+ * those that are down will be chosen to
* lead the gesture, and if none are down, `null` is returned. If [pointerId] is not down when
* [awaitPointerSlopOrCancellation] is called, then `null` is returned.
*
* When pointer slop is detected, [onPointerSlopReached] is called with the change and the distance
- * beyond the pointer slop. [PointerDirectionConfig.calculateDeltaChange] should return the position
- * change in the direction of the drag axis. If [onPointerSlopReached] does not consume the
+ * beyond the pointer slop. If [onPointerSlopReached] does not consume the
* position change, pointer slop will not have been considered detected and the detection will
* continue or, if it is consumed, the [PointerInputChange] that was consumed will be returned.
*
@@ -650,10 +739,10 @@
* `null` if all pointers are raised or the position change was consumed by another gesture
* detector.
*/
-internal suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
pointerId: PointerId,
pointerType: PointerType,
- pointerDirectionConfig: PointerDirectionConfig,
+ orientation: Orientation?,
onPointerSlopReached: (PointerInputChange, Offset) -> Unit,
): PointerInputChange? {
if (currentEvent.isPointerUp(pointerId)) {
@@ -661,8 +750,7 @@
}
val touchSlop = viewConfiguration.pointerSlop(pointerType)
var pointer: PointerId = pointerId
- var totalPositionChange = Offset.Zero
-
+ val touchSlopDetector = TouchSlopDetector(orientation)
while (true) {
val event = awaitPointerEvent()
val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
@@ -677,29 +765,8 @@
pointer = otherDown.id
}
} else {
- val currentPosition = dragEvent.position
- val previousPosition = dragEvent.previousPosition
-
- val positionChange = currentPosition - previousPosition
-
- totalPositionChange += positionChange
-
- val inDirection = pointerDirectionConfig.calculateDeltaChange(
- totalPositionChange
- )
-
- if (inDirection < touchSlop) {
- // verify that nothing else consumed the drag event
- awaitPointerEvent(PointerEventPass.Final)
- if (dragEvent.isConsumed) {
- return null
- }
- } else {
- val postSlopOffset = pointerDirectionConfig.calculatePostSlopOffset(
- totalPositionChange,
- touchSlop
- )
-
+ val postSlopOffset = touchSlopDetector.addPointerInputChange(dragEvent, touchSlop)
+ if (postSlopOffset != null) {
onPointerSlopReached(
dragEvent,
postSlopOffset
@@ -707,7 +774,13 @@
if (dragEvent.isConsumed) {
return dragEvent
} else {
- totalPositionChange = Offset.Zero
+ touchSlopDetector.reset()
+ }
+ } else {
+ // verify that nothing else consumed the drag event
+ awaitPointerEvent(PointerEventPass.Final)
+ if (dragEvent.isConsumed) {
+ return null
}
}
}
@@ -715,70 +788,77 @@
}
/**
- * Configures the calculations to get the change amount depending on the dragging type.
- * [calculatePostSlopOffset] will return the post offset slop when the touchSlop is reached.
+ * Detects if touch slop has been crossed after adding a series of [PointerInputChange].
+ * For every new [PointerInputChange] one should add it to this detector using
+ * [addPointerInputChange]. If the position change causes the touch slop to be crossed,
+ * [addPointerInputChange] will return true.
*/
-internal interface PointerDirectionConfig {
- fun calculateDeltaChange(offset: Offset): Float
- fun calculatePostSlopOffset(
- totalPositionChange: Offset,
- touchSlop: Float
- ): Offset
-}
+private class TouchSlopDetector(val orientation: Orientation? = null) {
-/**
- * Used for monitoring changes on X axis.
- */
-internal val HorizontalPointerDirectionConfig = object : PointerDirectionConfig {
- override fun calculateDeltaChange(offset: Offset): Float = abs(offset.x)
+ fun Offset.mainAxis() = if (orientation == Orientation.Horizontal) x else y
+ fun Offset.crossAxis() = if (orientation == Orientation.Horizontal) y else x
- override fun calculatePostSlopOffset(
- totalPositionChange: Offset,
+ /**
+ * The accumulation of drag deltas in this detector.
+ */
+ private var totalPositionChange: Offset = Offset.Zero
+
+ /**
+ * Adds [dragEvent] to this detector. If the accumulated position changes crosses the touch
+ * slop provided by [touchSlop], this method will return the post slop offset, that is the
+ * total accumulated delta change minus the touch slop value, otherwise this should return null.
+ */
+ fun addPointerInputChange(
+ dragEvent: PointerInputChange,
touchSlop: Float
- ): Offset {
- val finalMainPositionChange = totalPositionChange.x -
- (sign(totalPositionChange.x) * touchSlop)
- return Offset(finalMainPositionChange, totalPositionChange.y)
+ ): Offset? {
+ val currentPosition = dragEvent.position
+ val previousPosition = dragEvent.previousPosition
+ val positionChange = currentPosition - previousPosition
+ totalPositionChange += positionChange
+
+ val inDirection = if (orientation == null) {
+ totalPositionChange.getDistance()
+ } else {
+ totalPositionChange.mainAxis().absoluteValue
+ }
+
+ val hasCrossedSlop = inDirection >= touchSlop
+
+ return if (hasCrossedSlop) {
+ calculatePostSlopOffset(touchSlop)
+ } else {
+ null
+ }
+ }
+
+ /**
+ * Resets the accumulator associated with this detector.
+ */
+ fun reset() {
+ totalPositionChange = Offset.Zero
+ }
+
+ private fun calculatePostSlopOffset(touchSlop: Float): Offset {
+ return if (orientation == null) {
+ val touchSlopOffset =
+ totalPositionChange / totalPositionChange.getDistance() * touchSlop
+ // update postSlopOffset
+ totalPositionChange - touchSlopOffset
+ } else {
+ val finalMainAxisChange = totalPositionChange.mainAxis() -
+ (sign(totalPositionChange.mainAxis()) * touchSlop)
+ val finalCrossAxisChange = totalPositionChange.crossAxis()
+ if (orientation == Orientation.Horizontal) {
+ Offset(finalMainAxisChange, finalCrossAxisChange)
+ } else {
+ Offset(finalCrossAxisChange, finalMainAxisChange)
+ }
+ }
}
}
/**
- * Used for monitoring changes on Y axis.
- */
-internal val VerticalPointerDirectionConfig = object : PointerDirectionConfig {
- override fun calculateDeltaChange(offset: Offset): Float = abs(offset.y)
-
- override fun calculatePostSlopOffset(
- totalPositionChange: Offset,
- touchSlop: Float
- ): Offset {
- val finalMainPositionChange = totalPositionChange.y -
- (sign(totalPositionChange.y) * touchSlop)
- return Offset(totalPositionChange.x, finalMainPositionChange)
- }
-}
-
-/**
- * Used for monitoring changes on both X and Y axes.
- */
-internal val BidirectionalPointerDirectionConfig = object : PointerDirectionConfig {
- override fun calculateDeltaChange(offset: Offset): Float = offset.getDistance()
-
- override fun calculatePostSlopOffset(
- totalPositionChange: Offset,
- touchSlop: Float
- ): Offset {
- val touchSlopOffset =
- totalPositionChange / calculateDeltaChange(totalPositionChange) * touchSlop
- return totalPositionChange - touchSlopOffset
- }
-}
-
-internal fun Orientation.toPointerDirectionConfig(): PointerDirectionConfig =
- if (this == Orientation.Vertical) VerticalPointerDirectionConfig
- else HorizontalPointerDirectionConfig
-
-/**
* Waits for a long press by examining [pointerId].
*
* If that [pointerId] is raised (that is, the user lifts their finger), but another
@@ -839,7 +919,7 @@
// should technically never happen as we checked it above
finished = true
}
- // Pointer (id) stayed down.
+ // Pointer (id) stayed down.
} else {
longPress = event.changes.fastFirstOrNull { it.id == currentDown.id }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 5db29b1..92f3e6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -31,16 +31,11 @@
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
@@ -56,7 +51,6 @@
import kotlin.math.sign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -301,9 +295,10 @@
private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
private var reverseDirection: Boolean
) : DragGestureNode(
- canDrag,
- enabled,
- interactionSource
+ canDrag = canDrag,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = orientation
) {
override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
@@ -314,8 +309,6 @@
}
}
- override val pointerDirectionConfig = orientation.toPointerDirectionConfig()
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
this@DraggableNode.onDragStarted(this, startedPosition)
@@ -357,6 +350,7 @@
canDrag,
enabled,
interactionSource,
+ orientation,
resetPointerInputHandling
)
}
@@ -372,6 +366,7 @@
canDrag: (PointerInputChange) -> Boolean,
enabled: Boolean,
interactionSource: MutableInteractionSource?,
+ private var orientationLock: Orientation?
) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
protected var canDrag = canDrag
@@ -397,13 +392,6 @@
abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit)
/**
- * Returns the pointerDirectionConfig which specifies the main and cross axis deltas. This is
- * important when observing the delta change for Draggable, as we want to observe the change
- * in the main axis only.
- */
- abstract val pointerDirectionConfig: PointerDirectionConfig
-
- /**
* Passes the action needed when a drag starts. This gives the ability to pass the desired
* behavior from other nodes implementing AbstractDraggableNode
*/
@@ -478,62 +466,63 @@
// re-create tracker when pointer input block restarts. This lazily creates the tracker
// only when it is need.
val velocityTracker = VelocityTracker()
+ val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
+ { startEvent, initialDelta ->
+ if (canDrag.invoke(startEvent)) {
+ if (!isListeningForEvents) {
+ if (channel == null) {
+ channel = Channel(capacity = Channel.UNLIMITED)
+ }
+ startListeningForEvents()
+ }
+ val overSlopOffset = initialDelta
+ val xSign = sign(startEvent.position.x)
+ val ySign = sign(startEvent.position.y)
+ val adjustedStart = startEvent.position -
+ Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
+
+ channel?.trySend(DragStarted(adjustedStart))
+ }
+ }
+
+ val onDragEnd: (change: PointerInputChange) -> Unit = { upEvent ->
+ velocityTracker.addPointerInputChange(upEvent)
+ val maximumVelocity = currentValueOf(LocalViewConfiguration)
+ .maximumFlingVelocity
+ val velocity = velocityTracker.calculateVelocity(
+ Velocity(maximumVelocity, maximumVelocity)
+ )
+ velocityTracker.resetTracking()
+ channel?.trySend(DragStopped(velocity))
+ }
+
+ val onDragCancel: () -> Unit = {
+ channel?.trySend(DragCancelled)
+ }
+
+ val shouldAwaitTouchSlop: () -> Boolean = {
+ !startDragImmediately()
+ }
+
+ val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
+ { change, delta ->
+ velocityTracker.addPointerInputChange(change)
+ channel?.trySend(DragDelta(delta))
+ }
+
coroutineScope {
try {
- awaitPointerEventScope {
- while (isActive) {
- awaitDownAndSlop(
- _canDrag,
- ::startDragImmediately,
- velocityTracker,
- pointerDirectionConfig
- )?.let {
- /**
- * The gesture crossed the touch slop, events are now relevant
- * and should be propagated
- */
- if (!isListeningForEvents) {
- if (channel == null) {
- channel = Channel(capacity = Channel.UNLIMITED)
- }
- startListeningForEvents()
- }
- var isDragSuccessful = false
- try {
- isDragSuccessful = awaitDrag(
- it.first,
- it.second,
- velocityTracker,
- channel
- ) { event ->
- pointerDirectionConfig.calculateDeltaChange(
- event.positionChangeIgnoreConsumed()
- ) != 0f
- }
- } catch (cancellation: CancellationException) {
- isDragSuccessful = false
- if (!isActive) throw cancellation
- } finally {
- val maximumVelocity = currentValueOf(LocalViewConfiguration)
- .maximumFlingVelocity
- val event = if (isDragSuccessful) {
- val velocity = velocityTracker.calculateVelocity(
- Velocity(maximumVelocity, maximumVelocity)
- )
- velocityTracker.resetTracking()
- DragStopped(velocity)
- } else {
- DragCancelled
- }
- channel?.trySend(event)
- }
- }
- }
- }
- } catch (exception: CancellationException) {
- if (!isActive) {
- throw exception
- }
+ detectDragGestures(
+ orientationLock = orientationLock,
+ onDragStart = onDragStart,
+ onDragEnd = onDragEnd,
+ onDragCancel = onDragCancel,
+ shouldAwaitTouchSlop = shouldAwaitTouchSlop,
+ onDrag = onDrag
+ )
+ } catch (cancellation: CancellationException) {
+ channel?.trySend(DragCancelled)
+ if (!isActive) throw cancellation
}
}
}
@@ -580,9 +569,10 @@
canDrag: (PointerInputChange) -> Boolean = this.canDrag,
enabled: Boolean = this.enabled,
interactionSource: MutableInteractionSource? = this.interactionSource,
- isResetPointerInputHandling: Boolean = false
+ orientationLock: Orientation? = this.orientationLock,
+ shouldResetPointerInputHandling: Boolean = false
) {
- var resetPointerInputHandling = isResetPointerInputHandling
+ var resetPointerInputHandling = shouldResetPointerInputHandling
this.canDrag = canDrag
if (this.enabled != enabled) {
@@ -599,91 +589,17 @@
this.interactionSource = interactionSource
}
+ if (this.orientationLock != orientationLock) {
+ this.orientationLock = orientationLock
+ resetPointerInputHandling = true
+ }
+
if (resetPointerInputHandling) {
pointerInputNode?.resetPointerInputHandler()
}
}
}
-private suspend fun AwaitPointerEventScope.awaitDownAndSlop(
- canDrag: (PointerInputChange) -> Boolean,
- startDragImmediately: () -> Boolean,
- velocityTracker: VelocityTracker,
- pointerDirectionConfig: PointerDirectionConfig
-): Pair<PointerInputChange, Offset>? {
- val initialDown =
- awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
- return if (!canDrag(initialDown)) {
- null
- } else if (startDragImmediately()) {
- initialDown.consume()
- velocityTracker.addPointerInputChange(initialDown)
- // since we start immediately we don't wait for slop and the initial delta is 0
- initialDown to Offset.Zero
- } else {
- val down = awaitFirstDown(requireUnconsumed = false)
- velocityTracker.addPointerInputChange(down)
- var initialDelta = Offset.Zero
- val postPointerSlop = { event: PointerInputChange, offset: Offset ->
- velocityTracker.addPointerInputChange(event)
- event.consume()
- initialDelta = offset
- }
-
- val afterSlopResult = awaitPointerSlopOrCancellation(
- down.id,
- down.type,
- pointerDirectionConfig = pointerDirectionConfig,
- onPointerSlopReached = postPointerSlop
- )
-
- if (afterSlopResult != null) afterSlopResult to initialDelta else null
- }
-}
-
-private suspend fun AwaitPointerEventScope.awaitDrag(
- startEvent: PointerInputChange,
- initialDelta: Offset,
- velocityTracker: VelocityTracker,
- channel: SendChannel<DragEvent>?,
- hasDragged: (PointerInputChange) -> Boolean,
-): Boolean {
-
- val overSlopOffset = initialDelta
- val xSign = sign(startEvent.position.x)
- val ySign = sign(startEvent.position.y)
- val adjustedStart = startEvent.position -
- Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
- channel?.trySend(DragStarted(adjustedStart))
-
- channel?.trySend(DragDelta(initialDelta))
-
- return onDragOrUp(hasDragged, startEvent.id) { event ->
- // Velocity tracker takes all events, even UP
- velocityTracker.addPointerInputChange(event)
-
- // Dispatch only MOVE events
- if (!event.changedToUpIgnoreConsumed()) {
- val delta = event.positionChange()
- event.consume()
- channel?.trySend(DragDelta(delta))
- }
- }
-}
-
-private suspend fun AwaitPointerEventScope.onDragOrUp(
- hasDragged: (PointerInputChange) -> Boolean,
- pointerId: PointerId,
- onDrag: (PointerInputChange) -> Unit
-): Boolean {
- return drag(
- pointerId = pointerId,
- onDrag = onDrag,
- hasDragged = hasDragged,
- motionConsumed = { it.isConsumed }
- )?.let(onDrag) != null
-}
-
private class DefaultDraggableState(val onDelta: (Float) -> Unit) : DraggableState {
private val dragScope: DragScope = object : DragScope {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
index f9c9571..0c4b4c4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
@@ -267,9 +267,10 @@
private var onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
private var reverseDirection: Boolean
) : DragGestureNode(
- canDrag,
- enabled,
- interactionSource
+ canDrag = canDrag,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = null
) {
override suspend fun drag(
@@ -282,8 +283,6 @@
}
}
- override val pointerDirectionConfig = BidirectionalPointerDirectionConfig
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
this@Draggable2DNode.onDragStarted(this, startedPosition)
@@ -317,10 +316,11 @@
this.startDragImmediately = startDragImmediately
update(
- canDrag,
- enabled,
- interactionSource,
- resetPointerInputHandling
+ canDrag = canDrag,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = null,
+ shouldResetPointerInputHandling = resetPointerInputHandling
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index cf62744a..bcfaf97 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -273,7 +273,8 @@
) : DragGestureNode(
canDrag = CanDragCalculation,
enabled = enabled,
- interactionSource = interactionSource
+ interactionSource = interactionSource,
+ orientationLock = orientation
), ObserverModifierNode, CompositionLocalConsumerModifierNode,
FocusPropertiesModifierNode, KeyInputModifierNode {
@@ -333,9 +334,6 @@
scrollingLogic.dispatchDragEvents(forEachDelta)
}
- override val pointerDirectionConfig: PointerDirectionConfig
- get() = scrollingLogic.pointerDirectionConfig()
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -386,7 +384,13 @@
this.flingBehavior = flingBehavior
// update DragGestureNode
- update(CanDragCalculation, enabled, interactionSource, resetPointerInputHandling)
+ update(
+ canDrag = CanDragCalculation,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = if (scrollingLogic.isVertical()) Vertical else Horizontal,
+ shouldResetPointerInputHandling = resetPointerInputHandling
+ )
}
override fun onAttach() {
@@ -777,8 +781,6 @@
return resetPointerInputHandling
}
- fun pointerDirectionConfig(): PointerDirectionConfig = orientation.toPointerDirectionConfig()
-
fun isVertical(): Boolean = orientation == Vertical
}