Add `anchoredDrag` function and make animateTo and snapTo to be extension functions on AnchoredDraggableState
Convert AnchoredDraggableState to allow for custom drag gestures, while making aminateTo and snapTo to be extension functions, proving that its implementations are possible through public APIs.
Test: Added new tests and old test pass
Bug: n/a
Change-Id: I306c8c6933e3cf1c1b2a040ba2d80ca257f5b932
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableAnchorsTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableAnchorsTest.kt
index ea63b84..e398ae4 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableAnchorsTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableAnchorsTest.kt
@@ -21,13 +21,14 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
-import androidx.compose.material.AnchoredDraggableState.AnchorChangedCallback
-import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.AnchoredDraggableDefaults.ReconcileAnimationOnAnchorChangedCallback
import androidx.compose.material.AnchoredDraggableState
+import androidx.compose.material.AnchoredDraggableState.AnchorChangedCallback
+import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.A
import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.B
import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.C
+import androidx.compose.material.animateTo
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt
index d27b292..aa3c2e2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableGestureTest.kt
@@ -22,13 +22,14 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.material.AnchoredDraggableState
import androidx.compose.material.AutoTestFrameClock
import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.AnchoredDraggableState
+import androidx.compose.material.anchoredDraggable
import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.A
import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.B
import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.C
-import androidx.compose.material.anchoredDraggable
+import androidx.compose.material.animateTo
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.testutils.WithTouchSlop
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
index e0f833e6..d95fcb5 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -26,15 +26,17 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.material.AnchoredDraggableState.AnchorChangedCallback
-import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.AnchoredDraggableDefaults
import androidx.compose.material.AnchoredDraggableState
-import androidx.compose.material.rememberAnchoredDraggableState
+import androidx.compose.material.AnchoredDraggableState.AnchorChangedCallback
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.anchoredDraggable
import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.A
import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.B
import androidx.compose.material.anchoredDraggable.AnchoredDraggableTestValue.C
-import androidx.compose.material.anchoredDraggable
+import androidx.compose.material.animateTo
+import androidx.compose.material.rememberAnchoredDraggableState
+import androidx.compose.material.snapTo
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.SideEffect
@@ -43,6 +45,7 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -64,7 +67,9 @@
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Ignore
@@ -660,8 +665,8 @@
var anchorChangeHandlerInvoked = false
val testAnchorChangeHandler =
AnchorChangedCallback<AnchoredDraggableTestValue> { _, _, _ ->
- anchorChangeHandlerInvoked = true
- }
+ anchorChangeHandlerInvoked = true
+ }
val state = AnchoredDraggableState(
initialValue = A,
positionalThreshold = defaultPositionalThreshold,
@@ -677,8 +682,8 @@
var anchorChangeHandlerInvoked = false
val testAnchorChangedCallback =
AnchorChangedCallback<AnchoredDraggableTestValue> { _, _, _ ->
- anchorChangeHandlerInvoked = true
- }
+ anchorChangeHandlerInvoked = true
+ }
val state = AnchoredDraggableState(
initialValue = A,
positionalThreshold = defaultPositionalThreshold,
@@ -694,8 +699,8 @@
var anchorChangeHandlerInvoked = false
val testAnchorChangedCallback =
AnchorChangedCallback<AnchoredDraggableTestValue> { _, _, _ ->
- anchorChangeHandlerInvoked = true
- }
+ anchorChangeHandlerInvoked = true
+ }
val state = AnchoredDraggableState(
initialValue = A,
positionalThreshold = defaultPositionalThreshold,
@@ -711,6 +716,96 @@
}
@Test
+ fun anchoredDraggable_customDrag_updatesOffset() = runBlocking {
+
+ val state = AnchoredDraggableState(
+ initialValue = A,
+ positionalThreshold = defaultPositionalThreshold,
+ velocityThreshold = defaultVelocityThreshold
+ )
+ val anchors = mapOf(A to 0f, B to 200f, C to 300f)
+
+ state.updateAnchors(anchors)
+ state.anchoredDrag {
+ dragTo(150f)
+ }
+
+ assertThat(state.requireOffset()).isEqualTo(150f)
+
+ state.anchoredDrag {
+ dragTo(250f)
+ }
+ assertThat(state.requireOffset()).isEqualTo(250f)
+ }
+
+ @Test
+ fun anchoredDraggable_customDrag_updatesVelocity() = runBlocking {
+
+ val state = AnchoredDraggableState(
+ initialValue = A,
+ positionalThreshold = defaultPositionalThreshold,
+ velocityThreshold = defaultVelocityThreshold
+ )
+ val anchors = mapOf(A to 0f, B to 200f, C to 300f)
+
+ state.updateAnchors(anchors)
+ state.anchoredDrag {
+ dragTo(150f, lastKnownVelocity = 454f)
+ }
+ assertThat(state.lastVelocity).isEqualTo(454f)
+ }
+
+ @Test
+ fun anchoredDraggable_customDrag_targetValueUpdate() = runBlocking {
+ val clock = HandPumpTestFrameClock()
+ val dragScope = CoroutineScope(clock)
+
+ val state = AnchoredDraggableState(
+ initialValue = A,
+ positionalThreshold = defaultPositionalThreshold,
+ velocityThreshold = defaultVelocityThreshold
+ )
+ val anchors = mapOf(A to 0f, B to 200f, C to 300f)
+
+ state.updateAnchors(anchors)
+ dragScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ state.anchoredDrag(targetValue = C) {
+ while (isActive) {
+ withFrameNanos {
+ dragTo(200f)
+ }
+ }
+ }
+ }
+ clock.advanceByFrame()
+ assertThat(state.targetValue).isEqualTo(C)
+ dragScope.cancel()
+ }
+
+ @Test
+ fun anchoredDraggable_customDrag_anchorsPropagation() = runBlocking {
+ val clock = HandPumpTestFrameClock()
+ val dragScope = CoroutineScope(clock)
+
+ val state = AnchoredDraggableState(
+ initialValue = A,
+ positionalThreshold = defaultPositionalThreshold,
+ velocityThreshold = defaultVelocityThreshold
+ )
+ val anchors = mapOf(A to 0f, B to 200f, C to 300f)
+ var providedAnchors = emptyMap<AnchoredDraggableTestValue, Float>()
+
+ state.updateAnchors(anchors)
+ dragScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ state.anchoredDrag(targetValue = C) { anchors ->
+ providedAnchors = anchors
+ }
+ }
+ clock.advanceByFrame()
+ assertThat(providedAnchors).isEqualTo(anchors)
+ }
+
+ @Test
fun anchoredDraggable_updateAnchors_ongoingOffsetMutation_shouldNotUpdate() = runBlocking {
val clock = HandPumpTestFrameClock()
val animationScope = CoroutineScope(clock)
@@ -719,8 +814,8 @@
var anchorChangeHandlerInvoked = false
val testAnchorChangedCallback =
AnchorChangedCallback<AnchoredDraggableTestValue> { _, _, _ ->
- anchorChangeHandlerInvoked = true
- }
+ anchorChangeHandlerInvoked = true
+ }
val state = AnchoredDraggableState(
initialValue = A,
animationSpec = tween(animationDuration),
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
index b3a2bfe..6e204b8 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AnchoredDraggable.kt
@@ -71,7 +71,7 @@
reverseDirection: Boolean = false,
interactionSource: MutableInteractionSource? = null
) = draggable(
- state = state.swipeDraggableState,
+ state = state.draggableState,
orientation = orientation,
enabled = enabled,
interactionSource = interactionSource,
@@ -81,6 +81,26 @@
)
/**
+ * Scope used for suspending anchored drag blocks. Allows to set [AnchoredDraggableState.offset] to
+ * a new value.
+ *
+ * @see [AnchoredDraggableState.anchoredDrag] to learn how to start the anchored drag and get the
+ * access to this scope.
+ */
+internal interface AnchoredDragScope {
+ /**
+ * Assign a new value for an offset value for [AnchoredDraggableState].
+ *
+ * @param newOffset new value for [AnchoredDraggableState.offset].
+ * @param lastKnownVelocity last known velocity (if known)
+ */
+ fun dragTo(
+ newOffset: Float,
+ lastKnownVelocity: Float = 0f
+ )
+}
+
+/**
* State of the [anchoredDraggable] modifier.
*
* This contains necessary information about any ongoing drag or animation and provides methods
@@ -104,16 +124,19 @@
initialValue: T,
internal val positionalThreshold: (totalDistance: Float) -> Float,
internal val velocityThreshold: () -> Float,
- internal val animationSpec: AnimationSpec<Float> = AnchoredDraggableDefaults.AnimationSpec,
+ val animationSpec: AnimationSpec<Float> = AnchoredDraggableDefaults.AnimationSpec,
internal val confirmValueChange: (newValue: T) -> Boolean = { true }
) {
private val dragMutex = InternalMutatorMutex()
- internal val swipeDraggableState = object : DraggableState {
+ internal val draggableState = object : DraggableState {
+
private val dragScope = object : DragScope {
override fun dragBy(pixels: Float) {
- [email protected](pixels)
+ with(anchoredDragScope) {
+ dragTo(newOffsetForDelta(pixels))
+ }
}
}
@@ -121,7 +144,9 @@
dragPriority: MutatePriority,
block: suspend DragScope.() -> Unit
) {
- [email protected](dragPriority) { dragScope.block() }
+ [email protected](dragPriority) {
+ with(dragScope) { block() }
+ }
}
override fun dispatchRawDelta(delta: Float) {
@@ -250,7 +275,7 @@
val currentValueHasAnchor = anchors[currentValue] != null
if (previousAnchorsEmpty && currentValueHasAnchor) {
- snap(currentValue)
+ trySnapTo(currentValue)
} else {
onAnchorsChanged?.onAnchorsChanged(
previousTargetValue = previousTarget,
@@ -267,66 +292,6 @@
fun hasAnchorForValue(value: T): Boolean = anchors.containsKey(value)
/**
- * Snap to a [targetValue] without any animation.
- * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the
- * [targetValue] without updating the offset.
- *
- * @throws CancellationException if the interaction interrupted by another interaction like a
- * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
- *
- * @param targetValue The target value of the animation
- */
- suspend fun snapTo(targetValue: T) {
- drag { snap(targetValue) }
- }
-
- /**
- * Animate to a [targetValue].
- * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the
- * [targetValue] without updating the offset.
- *
- * @throws CancellationException if the interaction interrupted by another interaction like a
- * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
- *
- * @param targetValue The target value of the animation
- * @param velocity The velocity the animation should start with, [lastVelocity] by default
- */
- suspend fun animateTo(
- targetValue: T,
- velocity: Float = lastVelocity,
- ) {
- val targetOffset = anchors[targetValue]
- if (targetOffset != null) {
- try {
- drag {
- animationTarget = targetValue
- var prev = offset ?: 0f
- animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
- // Our onDrag coerces the value within the bounds, but an animation may
- // overshoot, for example a spring animation or an overshooting interpolator
- // We respect the user's intention and allow the overshoot, but still use
- // DraggableState's drag for its mutex.
- offset = value
- prev = value
- lastVelocity = velocity
- }
- lastVelocity = 0f
- }
- } finally {
- animationTarget = null
- val endOffset = requireOffset()
- val endState = anchors
- .entries
- .firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
- ?.key
- this.currentValue = endState ?: currentValue
- }
- } else {
- currentValue = targetValue
- }
- }
-
- /**
* Find the closest anchor taking into account the velocity and settle at it with an animation.
*/
suspend fun settle(velocity: Float) {
@@ -344,22 +309,6 @@
}
}
- /**
- * Drag by the [delta], coerce it in the bounds and dispatch it to the [AnchoredDraggableState].
- *
- * @return The delta the consumed by the [AnchoredDraggableState]
- */
- fun dispatchRawDelta(delta: Float): Float {
- val currentDragPosition = offset ?: 0f
- val potentiallyConsumed = currentDragPosition + delta
- val clamped = potentiallyConsumed.coerceIn(minOffset, maxOffset)
- val deltaToConsume = clamped - currentDragPosition
- if (abs(deltaToConsume) >= 0) {
- offset = ((offset ?: 0f) + deltaToConsume).coerceIn(minOffset, maxOffset)
- }
- return deltaToConsume
- }
-
private fun computeTarget(
offset: Float,
currentValue: T,
@@ -401,10 +350,97 @@
}
}
- private suspend fun drag(
- priority: MutatePriority = MutatePriority.Default,
- action: suspend () -> Unit
- ): Unit = coroutineScope { dragMutex.mutate(priority, action) }
+ private val anchoredDragScope: AnchoredDragScope = object : AnchoredDragScope {
+ override fun dragTo(newOffset: Float, lastKnownVelocity: Float) {
+ offset = newOffset
+ lastVelocity = lastKnownVelocity
+ }
+ }
+
+ /**
+ * Call this function to take control of drag logic and perform anchored drag.
+ *
+ * All actions that change the [offset] of this [AnchoredDraggableState] must be performed
+ * within an [anchoredDrag] block (even if they don't call any other methods on this object)
+ * in order to guarantee that mutual exclusion is enforced.
+ *
+ * If [anchoredDrag] is called from elsewhere with the [dragPriority] higher or equal to ongoing
+ * drag, ongoing drag will be canceled.
+ *
+ * @param dragPriority of the drag operation
+ * @param block perform anchored drag given the current anchor provided
+ */
+ suspend fun anchoredDrag(
+ dragPriority: MutatePriority = MutatePriority.Default,
+ block: suspend AnchoredDragScope.(anchors: Map<T, Float>) -> Unit
+ ): Unit = doAnchoredDrag(null, dragPriority, block)
+
+ /**
+ * Call this function to take control of drag logic and perform anchored drag.
+ *
+ * All actions that change the [offset] of this [AnchoredDraggableState] must be performed
+ * within an [anchoredDrag] block (even if they don't call any other methods on this object)
+ * in order to guarantee that mutual exclusion is enforced.
+ *
+ * This overload allows the caller to hint the target value that this [anchoredDrag] is intended
+ * to arrive to. This will set [AnchoredDraggableState.targetValue] to provided value so
+ * consumers can reflect it in their UIs.
+ *
+ * If [anchoredDrag] is called from elsewhere with the [dragPriority] higher or equal to ongoing
+ * drag, ongoing drag will be canceled.
+ *
+ * @param targetValue hint the target value that this [anchoredDrag] is intended to arrive to
+ * @param dragPriority of the drag operation
+ * @param block perform anchored drag given the current anchor provided
+ */
+ suspend fun anchoredDrag(
+ targetValue: T,
+ dragPriority: MutatePriority = MutatePriority.Default,
+ block: suspend AnchoredDragScope.(anchors: Map<T, Float>) -> Unit
+ ): Unit = doAnchoredDrag(targetValue, dragPriority, block)
+
+ private suspend fun doAnchoredDrag(
+ targetValue: T?,
+ dragPriority: MutatePriority,
+ block: suspend AnchoredDragScope.(anchors: Map<T, Float>) -> Unit
+ ) = coroutineScope {
+ if (targetValue == null || anchors.containsKey(targetValue)) {
+ try {
+ dragMutex.mutate(dragPriority) {
+ if (targetValue != null) animationTarget = targetValue
+ anchoredDragScope.block(anchors)
+ }
+ } finally {
+ if (targetValue != null) animationTarget = null
+ val endState = offset?.let { endOffset ->
+ anchors
+ .entries
+ .firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
+ ?.key
+ }
+ if (endState != null && confirmValueChange.invoke(endState)) {
+ currentValue = endState
+ }
+ }
+ } else if (confirmValueChange(targetValue)) {
+ currentValue = targetValue
+ }
+ }
+
+ internal fun newOffsetForDelta(delta: Float) =
+ ((offset ?: 0f) + delta).coerceIn(minOffset, maxOffset)
+
+ /**
+ * Drag by the [delta], coerce it in the bounds and dispatch it to the [AnchoredDraggableState].
+ *
+ * @return The delta the consumed by the [AnchoredDraggableState]
+ */
+ fun dispatchRawDelta(delta: Float): Float {
+ val newOffset = newOffsetForDelta(delta)
+ val oldOffset = offset ?: 0f
+ offset = newOffset
+ return newOffset - oldOffset
+ }
/**
* Attempt to snap synchronously. Snapping can happen synchronously when there is no other drag
@@ -413,15 +449,13 @@
*
* @return true if the synchronous snap was successful, or false if we couldn't snap synchronous
*/
- internal fun trySnapTo(targetValue: T): Boolean = dragMutex.tryMutate { snap(targetValue) }
-
- private fun snap(targetValue: T) {
- val targetOffset = anchors[targetValue]
- if (targetOffset != null) {
- dispatchRawDelta(targetOffset - (offset ?: 0f))
- currentValue = targetValue
- animationTarget = null
- } else {
+ internal fun trySnapTo(targetValue: T): Boolean = dragMutex.tryMutate {
+ with(anchoredDragScope) {
+ val targetOffset = anchors[targetValue]
+ if (targetOffset != null) {
+ dragTo(targetOffset)
+ animationTarget = null
+ }
currentValue = targetValue
}
}
@@ -480,6 +514,56 @@
}
/**
+ * Snap to a [targetValue] without any animation.
+ * If the [targetValue] is not in the set of anchors, the [AnchoredDraggableState.currentValue] will
+ * be updated to the [targetValue] without updating the offset.
+ *
+ * @throws CancellationException if the interaction interrupted by another interaction like a
+ * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
+ *
+ * @param targetValue The target value of the animation
+ */
+@ExperimentalMaterialApi
+internal suspend fun <T> AnchoredDraggableState<T>.snapTo(targetValue: T) {
+ anchoredDrag(targetValue = targetValue) { anchors ->
+ val targetOffset = anchors[targetValue]
+ if (targetOffset != null) dragTo(targetOffset)
+ }
+}
+
+/**
+ * Animate to a [targetValue].
+ * If the [targetValue] is not in the set of anchors, the [AnchoredDraggableState.currentValue] will
+ * be updated to the [targetValue] without updating the offset.
+ *
+ * @throws CancellationException if the interaction interrupted by another interaction like a
+ * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
+ *
+ * @param targetValue The target value of the animation
+ * @param velocity The velocity the animation should start with
+ */
+@ExperimentalMaterialApi
+internal suspend fun <T> AnchoredDraggableState<T>.animateTo(
+ targetValue: T,
+ velocity: Float = this.lastVelocity,
+) {
+ anchoredDrag(targetValue = targetValue) { anchors ->
+ val targetOffset = anchors[targetValue]
+ if (targetOffset != null) {
+ var prev = offset ?: 0f
+ animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
+ // Our onDrag coerces the value within the bounds, but an animation may
+ // overshoot, for example a spring animation or an overshooting interpolator
+ // We respect the user's intention and allow the overshoot, but still use
+ // DraggableState's drag for its mutex.
+ dragTo(value, velocity)
+ prev = value
+ }
+ }
+ }
+}
+
+/**
* Create and remember a [AnchoredDraggableState].
*
* @param initialValue The initial value.
@@ -562,20 +646,20 @@
state: AnchoredDraggableState<T>,
scope: CoroutineScope
) = AnchorChangedCallback<T> { previousTarget, previousAnchors, newAnchors ->
- val previousTargetOffset = previousAnchors[previousTarget]
- val newTargetOffset = newAnchors[previousTarget]
- if (previousTargetOffset != newTargetOffset) {
- if (newTargetOffset != null) {
- scope.launch {
- state.animateTo(previousTarget, state.lastVelocity)
- }
- } else {
- scope.launch {
- state.snapTo(newAnchors.closestAnchor(offset = state.requireOffset()))
- }
+ val previousTargetOffset = previousAnchors[previousTarget]
+ val newTargetOffset = newAnchors[previousTarget]
+ if (previousTargetOffset != newTargetOffset) {
+ if (newTargetOffset != null) {
+ scope.launch {
+ state.animateTo(previousTarget, state.lastVelocity)
+ }
+ } else {
+ scope.launch {
+ state.snapTo(newAnchors.closestAnchor(offset = state.requireOffset()))
}
}
}
+ }
}
private fun <T> Map<T, Float>.closestAnchor(