Preliminary implementation of DragGestureDetector
Bug: 124533040
Test: DragGestureDetectorTest
Change-Id: I936869fe9f52f85f63f7c4e54cb1e771ac276fba
diff --git a/ui/framework/src/main/java/androidx/ui/core/Wrapper.kt b/ui/framework/src/main/java/androidx/ui/core/Wrapper.kt
index a53f5f0..7a1217d 100644
--- a/ui/framework/src/main/java/androidx/ui/core/Wrapper.kt
+++ b/ui/framework/src/main/java/androidx/ui/core/Wrapper.kt
@@ -75,7 +75,7 @@
val ContextAmbient = Ambient.of<Context>()
-internal val DensityAmbient = Ambient.of<Density>()
+val DensityAmbient = Ambient.of<Density>()
/**
* [ambient] to get a [Density] object from an internal [DensityAmbient].
diff --git a/ui/framework/src/main/java/androidx/ui/core/gesture/DragGestureDetector.kt b/ui/framework/src/main/java/androidx/ui/core/gesture/DragGestureDetector.kt
new file mode 100644
index 0000000..b1acdca
--- /dev/null
+++ b/ui/framework/src/main/java/androidx/ui/core/gesture/DragGestureDetector.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.core.gesture
+
+import androidx.ui.core.DensityAmbient
+import androidx.ui.core.Direction
+import androidx.ui.core.PointerEventPass
+import androidx.ui.core.PointerInput
+import androidx.ui.core.PointerInputChange
+import androidx.ui.core.PxPosition
+import androidx.ui.core.changedToDown
+import androidx.ui.core.changedToDownIgnoreConsumed
+import androidx.ui.core.changedToUp
+import androidx.ui.core.changedToUpIgnoreConsumed
+import androidx.ui.core.consumeDownChange
+import androidx.ui.core.consumePositionChange
+import androidx.ui.core.gesture.util.VelocityTracker
+import androidx.ui.core.ipx
+import androidx.ui.core.positionChange
+import androidx.ui.core.px
+import androidx.ui.core.withDensity
+import com.google.r4a.Children
+import com.google.r4a.Component
+import com.google.r4a.composer
+
+open class DragObserver {
+ open fun onStart() {}
+ open fun onDrag(dragDistance: PxPosition) = dragDistance
+ open fun onStop(velocity: PxPosition) {}
+}
+
+// TODO(shepshapard): Convert to functional component with effects once effects are ready.
+/**
+ *
+ */
+class DragGestureDetector(
+ @Children var children: () -> Unit
+) : Component() {
+
+ private val recognizer = DragGestureRecognizer()
+
+ var canDrag: ((Direction) -> Boolean)?
+ get() = recognizer.canDrag
+ set(value) {
+ recognizer.canDrag = value
+ }
+ // TODO(b/129784010): Consider also allowing onStart, onDrag, and onEnd to be set individually.
+ var dragObserver
+ get() = recognizer.dragObserver
+ set(value) {
+ recognizer.dragObserver = value
+ }
+
+ override fun compose() {
+ <DensityAmbient.Consumer> density ->
+ withDensity(density) {
+ recognizer.touchSlop = TouchSlop.toIntPx()
+ <PointerInput pointerInputHandler=recognizer.pointerInputHandler>
+ <children />
+ </PointerInput>
+ }
+ </DensityAmbient.Consumer>
+ }
+}
+
+internal class DragGestureRecognizer {
+ private val pointerTrackers: MutableMap<Int, PointerTrackingData> = mutableMapOf()
+ private var passedSlop = false
+ private var pointerCount = 0
+ var touchSlop = 0.ipx
+
+ var canDrag: ((Direction) -> Boolean)? = null
+ var dragObserver: DragObserver? = null
+
+ val pointerInputHandler = { pointerInputChange: PointerInputChange, pass: PointerEventPass ->
+ var change: PointerInputChange = pointerInputChange
+
+ if (pass == PointerEventPass.InitialDown && change.changedToDownIgnoreConsumed()) {
+ pointerCount++
+ }
+
+ if (pass == PointerEventPass.InitialDown && change.changedToDown() && passedSlop) {
+ // If we are passedSlop, we are actively dragging so we want to prevent any children
+ // from reacting to any down change.
+ change = change.consumeDownChange()
+ }
+
+ if (pass == PointerEventPass.PostUp) {
+ if (change.changedToUpIgnoreConsumed()) {
+ // This pointer is up (consumed or not), so we should stop tracking information
+ // about it. Get a reference for the velocity tracker in case this is the last
+ // pointer and thus we are going to fling.
+ val velocityTracker = pointerTrackers.remove(change.id)?.velocityTracker
+ if (pointerCount == 1) {
+ if (passedSlop && change.changedToUp()) {
+ // There is one pointer, we have passed slop, and there was an unconsumed
+ // up event, so we should fire the onStop with the velocity tracked
+ // for the last pointer.
+
+ // TODO(shepshapard): handle the case that the velocity tracker for the
+ // given pointer is null, by throwing an exception or printing a warning,
+ // or something else.
+ val velocity =
+ velocityTracker?.calculateVelocity()?.pixelsPerSecond
+ ?: PxPosition.Origin
+ velocityTracker?.resetTracking()
+ dragObserver?.onStop(PxPosition(velocity.x, velocity.y))
+
+ // We responded to the up change, so consume it.
+ change = change.consumeDownChange()
+ }
+ // The last pointer is up whether or not up was consumed, so we should reset
+ // that we passed slop.
+ passedSlop = false
+ }
+ } else if (change.changedToDownIgnoreConsumed()) {
+ // If a pointer has changed to down, we should start tracking information about it.
+ pointerTrackers[change.id] = PointerTrackingData()
+ .apply {
+ velocityTracker.addPosition(
+ change.current.timestamp!!,
+ PxPosition(
+ change.current.position!!.dx.px,
+ change.current.position!!.dy.px
+ )
+ )
+ }
+ } else if (change.current.down) {
+ // TODO(shepshapard): handle the case that the pointerTrackingData is null, either
+ // with an exception or a logged error, or something else.
+ val pointerTracker: PointerTrackingData? = pointerTrackers[change.id]
+
+ if (pointerTracker != null) {
+ // If the pointer is currently down, we should track its velocity.
+ pointerTracker.velocityTracker.addPosition(
+ change.current.timestamp!!,
+ PxPosition(
+ change.current.position!!.dx.px,
+ change.current.position!!.dy.px
+ )
+ )
+
+ val dx = change.positionChange().dx
+ val dy = change.positionChange().dy
+
+ // If we aren't passed slop, calculate things related to slop, and start drag
+ // if we do pass touch slop.
+ if (!passedSlop) {
+ // TODO(shepshapard): I believe the logic in this block could be simplified
+ // to be much more clear. Will need to revisit. The need to make
+ // improvements may be rendered obsolete with upcoming changes however.
+
+ val directionX = when {
+ dx == 0f -> null
+ dx < 0f -> Direction.LEFT
+ else -> Direction.RIGHT
+ }
+ val directionY = when {
+ dy == 0f -> null
+ dy < 0f -> Direction.UP
+ else -> Direction.DOWN
+ }
+
+ val canDragX =
+ if (directionX != null) {
+ canDrag?.invoke(directionX) ?: true
+ } else false
+ val canDragY =
+ if (directionY != null) {
+ canDrag?.invoke(directionY) ?: true
+ } else false
+
+ pointerTracker.dxUnderSlop += dx
+ pointerTracker.dyUnderSlop += dy
+
+ val passedSlopX =
+ canDragX && Math.abs(pointerTracker.dxUnderSlop) > touchSlop.value
+ val passedSlopY =
+ canDragY && Math.abs(pointerTracker.dyUnderSlop) > touchSlop.value
+
+ if (passedSlopX || passedSlopY) {
+ passedSlop = true
+ dragObserver?.onStart()
+ } else {
+ if (!canDragX &&
+ ((directionX == Direction.LEFT && pointerTracker.dxUnderSlop < 0) ||
+ (directionX == Direction.RIGHT &&
+ pointerTracker.dxUnderSlop > 0))
+ ) {
+ pointerTracker.dxUnderSlop = 0f
+ }
+ if (!canDragY &&
+ ((directionY == Direction.LEFT && pointerTracker.dyUnderSlop < 0) ||
+ (directionY == Direction.DOWN &&
+ pointerTracker.dyUnderSlop > 0))
+ ) {
+ pointerTracker.dyUnderSlop = 0f
+ }
+ }
+ }
+
+ // At this point, check to see if we have passed touch slop, and if we have
+ // go ahead and drag and consume.
+ if (passedSlop) {
+ change = dragObserver?.run {
+ val (consumedDx, consumedDy) = onDrag(
+ PxPosition(
+ dx.px,
+ dy.px
+ )
+ )
+ pointerInputChange.consumePositionChange(
+ consumedDx.value,
+ consumedDy.value
+ )
+ } ?: pointerInputChange
+ }
+ }
+ }
+ }
+
+ if (pass == PointerEventPass.PostDown && change.changedToUpIgnoreConsumed()) {
+ pointerCount--
+ }
+
+ change
+ }
+
+ internal data class PointerTrackingData(
+ val velocityTracker: VelocityTracker = VelocityTracker(),
+ var dxUnderSlop: Float = 0f,
+ var dyUnderSlop: Float = 0f
+ )
+}
\ No newline at end of file
diff --git a/ui/framework/src/main/java/androidx/ui/core/gesture/constants.kt b/ui/framework/src/main/java/androidx/ui/core/gesture/constants.kt
new file mode 100644
index 0000000..714bd82
--- /dev/null
+++ b/ui/framework/src/main/java/androidx/ui/core/gesture/constants.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.core.gesture
+
+import androidx.ui.core.Duration
+import androidx.ui.core.dp
+import androidx.ui.core.milliseconds
+
+/**
+ * Modeled after Android's ViewConfiguration:
+ * https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/ViewConfiguration.java
+ */
+
+/**
+ * The time that must elapse before a tap gesture sends onTapDown, if there's
+ * any doubt that the gesture is a tap.
+ */
+val PressTimeout: Duration = 100.milliseconds
+
+/**
+ * Maximum length of time between a tap down and a tap up for the gesture to be
+ * considered a tap. (Currently not honored by the TapGestureRecognizer.)
+ */
+// TODO(shepshapard): Remove this, or implement a hover-tap gesture recognizer which
+// uses this.
+val HoverTapTimeout: Duration = 150.milliseconds
+
+/**
+ * Maximum distance between the down and up pointers for a tap. (Currently not
+ * honored by the [TapGestureRecognizer]; [PrimaryPointerGestureRecognizer],
+ * which TapGestureRecognizer inherits from, uses [kTouchSlop].)
+ */
+// TODO(shepshapard): Remove this or implement it correctly.
+val HoverTapSlop = 20.dp
+
+/** The time before a long press gesture attempts to win. */
+val LongPressTimeout: Duration = 500.milliseconds
+
+/**
+ * The maximum time from the start of the first tap to the start of the second
+ * tap in a double-tap gesture.
+ */
+// TODO(shepshapard): In Android, this is actually the time from the first's up event
+// to the second's down event, according to the ViewConfiguration docs.
+val DoubleTapTimeout: Duration = 300.milliseconds
+
+/**
+ * The minimum time from the end of the first tap to the start of the second
+ * tap in a double-tap gesture. (Currently not honored by the
+ * DoubleTapGestureRecognizer.)
+ */
+// TODO(shepshapard): Either implement this or remove the constant.
+val DoubleTapMinTime: Duration = 40.milliseconds
+
+/**
+ * The distance a touch has to travel for the framework to be confident that
+ * the gesture is a scroll gesture, or, inversely, the maximum distance that a
+ * touch can travel before the framework becomes confident that it is not a
+ * tap.
+ */
+// This value was empirically derived. We started at 8.0 and increased it to
+// 18.0 after getting complaints that it was too difficult to hit targets.
+val TouchSlop = 18.dp
+
+/**
+ * The maximum distance that the first touch in a double-tap gesture can travel
+ * before deciding that it is not part of a double-tap gesture.
+ * DoubleTapGestureRecognizer also restricts the second touch to this distance.
+ */
+val DoubleTapTouchSlop = TouchSlop
+
+/**
+ * Distance between the initial position of the first touch and the start
+ * position of a potential second touch for the second touch to be considered
+ * the second touch of a double-tap gesture.
+ */
+val DoubleTapSlop = 100.dp
+
+/**
+ * The time for which zoom controls (e.g. in a map interface) are to be
+ * displayed on the screen, from the moment they were last requested.
+ */
+val ZoomControlsTimeout: Duration = 3000.milliseconds
+
+/**
+ * The distance a touch has to travel for the framework to be confident that
+ * the gesture is a paging gesture. (Currently not used, because paging uses a
+ * regular drag gesture, which uses kTouchSlop.)
+ */
+// TODO(shepshapard): Create variants of HorizontalDragGestureRecognizer et al for
+// paging, which use this constant.
+val PagingTouchSlop = TouchSlop * 2.dp
+
+/**
+ * The distance a touch has to travel for the framework to be confident that
+ * the gesture is a panning gesture.
+ */
+val PanSlop = TouchSlop * 2.dp
+
+/**
+ * The distance a touch has to travel for the framework to be confident that
+ * the gesture is a scale gesture.
+ */
+val ScaleSlop = TouchSlop
+
+/**
+ * The margin around a dialog, popup menu, or other window-like widget inside
+ * which we do not consider a tap to dismiss the widget. (Not currently used.)
+ */
+// TODO(shepshapard): Make ModalBarrier support this.
+val WindowTouchSlop = 16.dp
+
+/**
+ * The minimum velocity for a touch to consider that touch to trigger a fling
+ * gesture.
+ */
+// TODO(shepshapard): Make sure nobody has their own version of this.
+val MinFlingVelocity = 50.dp // Logical pixels / second
+
+/** Drag gesture fling velocities are clipped to this value. */
+// TODO(shepshapard): Make sure nobody has their own version of this.
+val MaxFlingVelocity = 8000.dp // Logical pixels / second
+
+/**
+ * The maximum time from the start of the first tap to the start of the second
+ * tap in a jump-tap gesture.
+ */
+// TODO(shepshapard): Implement jump-tap gestures.
+val JumpTapTimeout: Duration = 500.milliseconds
diff --git a/ui/framework/src/main/java/androidx/ui/core/gesture/util/VelocityTracker.kt b/ui/framework/src/main/java/androidx/ui/core/gesture/util/VelocityTracker.kt
index 9f969d7..2275aa1 100644
--- a/ui/framework/src/main/java/androidx/ui/core/gesture/util/VelocityTracker.kt
+++ b/ui/framework/src/main/java/androidx/ui/core/gesture/util/VelocityTracker.kt
@@ -17,14 +17,11 @@
package androidx.ui.core.gesture.util
import androidx.ui.core.Duration
-import androidx.ui.core.Px
import androidx.ui.core.PxPosition
import androidx.ui.core.Timestamp
import androidx.ui.core.Velocity
-import androidx.ui.core.getDistance
import androidx.ui.core.inMilliseconds
import androidx.ui.core.px
-import java.lang.IllegalArgumentException
import kotlin.math.absoluteValue
private const val AssumePointerMoveStoppedMilliseconds: Int = 40
@@ -63,29 +60,11 @@
}
/**
- * Computes the velocity of the pointer at the time of the last provided data point.
+ * Computes the estimated velocity of the pointer at the time of the last provided data point.
*
* This can be expensive. Only call this when you need the velocity.
- *
- * @param min Pixels per second. If the velocity falls below this, the returned velocity will
- * be 0.
- * @param max Pixels per second. If the velocity exceeds this, the returned velocity will be 0.
- * @return `null` if there is no data from which to compute an estimate.
*/
- fun calculateVelocity(
- min: Px = 0f.px,
- max: Px = Float.MAX_VALUE.px
- ): Velocity {
- val estimate: VelocityEstimate = getVelocityEstimate()
-
- // If we are outside the bounds of allowable velocity, return a velocity of 0.
- val distance = estimate.pixelsPerSecond.getDistance()
- if (distance < min || distance > max) {
- return Velocity.Zero
- }
-
- return Velocity(pixelsPerSecond = estimate.pixelsPerSecond)
- }
+ fun calculateVelocity() = Velocity(pixelsPerSecond = getVelocityEstimate().pixelsPerSecond)
/**
* Clears the tracked positions added by [addPosition].
diff --git a/ui/framework/src/test/java/androidx/ui/core/gesture/DragGestureDetectorTest.kt b/ui/framework/src/test/java/androidx/ui/core/gesture/DragGestureDetectorTest.kt
new file mode 100644
index 0000000..119b417
--- /dev/null
+++ b/ui/framework/src/test/java/androidx/ui/core/gesture/DragGestureDetectorTest.kt
@@ -0,0 +1,754 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.core.gesture
+
+import androidx.ui.core.Direction
+import androidx.ui.core.Duration
+import androidx.ui.core.PxPosition
+import androidx.ui.core.anyPositionChangeConsumed
+import androidx.ui.core.consumeDownChange
+import androidx.ui.core.consumePositionChange
+import androidx.ui.core.ipx
+import androidx.ui.core.millisecondsToTimestamp
+import androidx.ui.core.px
+import androidx.ui.testutils.consume
+import androidx.ui.testutils.down
+import androidx.ui.testutils.invokeOverAllPasses
+import androidx.ui.testutils.moveBy
+import androidx.ui.testutils.moveTo
+import androidx.ui.testutils.up
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+// TODO(shepshapard): Write the following tests.
+// Verify correct shape of slop area (should it be a square or circle)?
+// Test for cases with more than one pointer
+// Test for cases where things are reset when last pointer goes up
+// Verify all methods called during onPostUp
+// Verify default behavior when no callback provided for recognizer or canDrag
+
+// Changing this value will break tests that expect the value to be 10.
+private const val TestTouchSlop = 10
+
+@RunWith(JUnit4::class)
+class DragGestureDetectorTest {
+
+ private lateinit var recognizer: DragGestureRecognizer
+ private lateinit var canDragMockTrue: MockCanDrag
+ private lateinit var log: MutableList<LogItem>
+
+ @Before
+ fun setup() {
+ log = mutableListOf()
+ recognizer = DragGestureRecognizer()
+ recognizer.touchSlop = TestTouchSlop.ipx
+ canDragMockTrue = MockCanDrag(Direction.values(), log)
+ }
+
+ // Verify the circumstances under which canDrag should not be called.
+
+ @Test
+ fun pointerInputHandler_down_canDragNotCalled() {
+ recognizer.canDrag = canDragMockTrue
+ recognizer.pointerInputHandler.invokeOverAllPasses(down())
+ assertThat(log).isEmpty()
+ }
+
+ @Test
+ fun pointerInputHandler_downMoveFullyConsumed_canDragNotCalled() {
+ recognizer.canDrag = canDragMockTrue
+
+ val down = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ recognizer.pointerInputHandler.invokeOverAllPasses(
+ down.moveBy(Duration(milliseconds = 10), 3f, 5f).consume(3f, 5f)
+ )
+
+ assertThat(log).isEmpty()
+ }
+
+ // Verify the circumstances under which canDrag should be called.
+
+ @Test
+ fun pointerInputHandler_downMove1Dimension_canDragCalledOnce() {
+ recognizer.canDrag = canDragMockTrue
+
+ val down = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ recognizer.pointerInputHandler.invokeOverAllPasses(
+ down.moveBy(Duration(milliseconds = 10), 3f, 0f)
+ )
+
+ assertThat(log.filter { it.methodName == "canDrag" }).hasSize(1)
+ }
+
+ @Test
+ fun pointerInputHandler_downMove2Dimensions_canDragCalledTwice() {
+ recognizer.canDrag = canDragMockTrue
+
+ val down = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ recognizer.pointerInputHandler.invokeOverAllPasses(
+ down.moveBy(Duration(milliseconds = 10), 3f, 5f)
+ )
+
+ assertThat(log.filter { it.methodName == "canDrag" }).hasSize(2)
+ }
+
+ @Test
+ fun pointerInputHandler_downMoveOneDimensionPartiallyConsumed_canDragCalledOnce() {
+ recognizer.canDrag = canDragMockTrue
+
+ val down = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ recognizer.pointerInputHandler.invokeOverAllPasses(
+ down.moveBy(Duration(milliseconds = 10), 0f, 5f).consume(0f, 4f)
+ )
+
+ assertThat(log.filter { it.methodName == "canDrag" }).hasSize(1)
+ }
+
+ @Test
+ fun pointerInputHandler_downMoveTwoDimensionPartiallyConsumed_canDragCalledTwice() {
+ recognizer.canDrag = canDragMockTrue
+ val down = down()
+
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ recognizer.pointerInputHandler.invokeOverAllPasses(
+ down.moveBy(Duration(milliseconds = 10), 3f, 5f).consume(2f, 4f)
+ )
+
+ assertThat(log.filter { it.methodName == "canDrag" }).hasSize(2)
+ }
+
+ @Test
+ fun pointerInputHandler_dragPastTouchSlopOneDimensionAndDrag3MoreTimes_canDragCalledOnce() {
+ val justBeyondSlop = (TestTouchSlop + 1).toFloat()
+ recognizer.canDrag = canDragMockTrue
+
+ val down = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ var move = down.moveTo(10L.millisecondsToTimestamp(), 0f, justBeyondSlop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(move)
+ repeat(3) {
+ move = move.moveBy(Duration(milliseconds = 10), 0f, 1f)
+ recognizer.pointerInputHandler.invokeOverAllPasses(move)
+ }
+
+ assertThat(log.filter { it.methodName == "canDrag" }).hasSize(1)
+ }
+
+ @Test
+ fun pointerInputHandler_downMoveUnderSlop3Times_canDragCalled3Times() {
+ val thirdSlop = TestTouchSlop.toFloat() / 3
+ recognizer.canDrag = canDragMockTrue
+
+ var event = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(event)
+ repeat(3) {
+ event = event.moveBy(Duration(milliseconds = 10), 0f, thirdSlop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(event)
+ }
+
+ assertThat(log.filter { it.methodName == "canDrag" }).hasSize(3)
+ }
+
+ @Test
+ fun pointerInputHandler_moveBeyondSlopThenIntoTouchSlopAreaAndOutAgain_canDragCalledOnce() {
+ val beyondTouchSlop = (TestTouchSlop + 1).toFloat()
+ recognizer.canDrag = canDragMockTrue
+
+ var event = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(event)
+ // Out of touch slop region
+ event = event.moveBy(Duration(milliseconds = 10), 0f, beyondTouchSlop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(event)
+ // Back into touch slop region
+ event = event.moveBy(Duration(milliseconds = 10), 0f, -beyondTouchSlop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(event)
+ // Out of touch slop region again
+ event = event.moveBy(Duration(milliseconds = 10), 0f, beyondTouchSlop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(event)
+
+ assertThat(log.filter { it.methodName == "canDrag" }).hasSize(1)
+ }
+
+ // Verification of correctness of values passed to onDrag.
+
+ @Test
+ fun pointerInputHandler_canDragCalledWithCorrectDirection() {
+ pointerInputHandler_canDragCalledWithCorrectDirection(-1f, 0f, arrayOf(Direction.LEFT))
+ pointerInputHandler_canDragCalledWithCorrectDirection(0f, -1f, arrayOf(Direction.UP))
+ pointerInputHandler_canDragCalledWithCorrectDirection(1f, 0f, arrayOf(Direction.RIGHT))
+ pointerInputHandler_canDragCalledWithCorrectDirection(0f, 1f, arrayOf(Direction.DOWN))
+
+ pointerInputHandler_canDragCalledWithCorrectDirection(
+ -1f,
+ -1f,
+ arrayOf(Direction.LEFT, Direction.UP)
+ )
+ pointerInputHandler_canDragCalledWithCorrectDirection(
+ -1f,
+ 1f,
+ arrayOf(Direction.LEFT, Direction.DOWN)
+ )
+ pointerInputHandler_canDragCalledWithCorrectDirection(
+ 1f,
+ -1f,
+ arrayOf(Direction.RIGHT, Direction.UP)
+ )
+ pointerInputHandler_canDragCalledWithCorrectDirection(
+ 1f,
+ 1f,
+ arrayOf(Direction.RIGHT, Direction.DOWN)
+ )
+ }
+
+ private fun pointerInputHandler_canDragCalledWithCorrectDirection(
+ dx: Float,
+ dy: Float,
+ expectedDirections: Array<Direction>
+ ) {
+ log.clear()
+ recognizer.canDrag = canDragMockTrue
+
+ val down = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ recognizer.pointerInputHandler.invokeOverAllPasses(
+ down.moveBy(Duration(milliseconds = 10), dx, dy)
+ )
+
+ assertThat(log).hasSize(expectedDirections.size)
+ expectedDirections.forEach {
+ log.contains(LogItem("canDrag", direction = it))
+ }
+ }
+
+ // Verify the circumstances under which onStart/OnDrag should not be called.
+
+ // TODO(b/129701831): This test assumes that if a pointer moves by slop in both x and y, we are
+ // still under slop even though sqrt(slop^2 + slop^2) > slop. This may be inaccurate and this
+ // test may therefore need to be updated.
+ @Test
+ fun pointerInputHandler_downMoveWithinSlop_onStartAndOnDragNotCalled() {
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ val down = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ recognizer.pointerInputHandler.invokeOverAllPasses(
+ down.moveBy(
+ Duration(milliseconds = 10),
+ TestTouchSlop.toFloat(),
+ TestTouchSlop.toFloat()
+ )
+ )
+
+ assertThat(log.filter { it.methodName == "onStart" }).hasSize(0)
+ assertThat(log.filter { it.methodName == "onDrag" }).hasSize(0)
+ }
+
+ @Test
+ fun pointerInputHandler_moveBeyondSlopInUnsupportedDirection_onStartAndOnDragNotCalled() {
+ val beyondSlop = (TestTouchSlop + 1).toFloat()
+ recognizer.canDrag = MockCanDrag(arrayOf(), log)
+ recognizer.dragObserver = MockDragObserver(log)
+
+ val down = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ recognizer.pointerInputHandler.invokeOverAllPasses(
+ down.moveBy(
+ Duration(milliseconds = 10),
+ beyondSlop,
+ beyondSlop
+ )
+ )
+
+ assertThat(log.filter { it.methodName == "onStart" }).hasSize(0)
+ assertThat(log.filter { it.methodName == "onDrag" }).hasSize(0)
+ }
+
+ // TODO(b/129701831): This test assumes that if a pointer moves by slop in both x and y, we are
+ // still under slop even though sqrt(slop^2 + slop^2) > slop. This may be inaccurate and this
+ // test may therefore need to be updated.
+ @Test
+ fun pointerInputHandler_moveAroundWithinSlop_onStartAndOnDragNotCalled() {
+ val slop = TestTouchSlop.toFloat()
+ val doubleSlop = slop * 2
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ // Go around the border of the touch slop area
+
+ // To top left
+ change = change.moveTo(10L.millisecondsToTimestamp(), -slop, -slop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ // To bottom left
+ change = change.moveTo(20L.millisecondsToTimestamp(), -slop, slop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ // To bottom right
+ change = change.moveTo(30L.millisecondsToTimestamp(), slop, slop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ // To top right
+ change = change.moveTo(40L.millisecondsToTimestamp(), slop, -slop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ // Jump from corner to opposite corner and back
+
+ // To bottom left
+ change = change.moveTo(50L.millisecondsToTimestamp(), -slop, slop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ // To top right
+ change = change.moveTo(60L.millisecondsToTimestamp(), slop, -slop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ // Move the other diagonal
+
+ // To top left
+ change = change.moveTo(70L.millisecondsToTimestamp(), -slop, -slop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ // Jump from corner to opposite corner and back
+
+ // To bottom right
+ change = change.moveTo(80L.millisecondsToTimestamp(), slop, slop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ // To top left
+ change = change.moveTo(90L.millisecondsToTimestamp(), -slop, -slop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(log.filter { it.methodName == "onStart" }).hasSize(0)
+ assertThat(log.filter { it.methodName == "onDrag" }).hasSize(0)
+ }
+
+ // Verify the circumstances under which onStart/OnDrag should be called.
+
+ @Test
+ fun pointerInputHandler_movePassedSlop_onStartAndOnDragCalledOnce() {
+ val beyondTouchSlop = (TestTouchSlop + 1).toFloat()
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+ val down = down()
+
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ recognizer.pointerInputHandler.invokeOverAllPasses(
+ down.moveBy(
+ Duration(milliseconds = 100),
+ beyondTouchSlop,
+ 0f
+ )
+ )
+
+ assertThat(log.filter { it.methodName == "onStart" }).hasSize(1)
+ assertThat(log.filter { it.methodName == "onDrag" }).hasSize(1)
+ }
+
+ @Test
+ fun pointerInputHandler_passSlopThenIntoSlopAreaThenOut_onStartCalledOnceAndOnDrag3() {
+ val beyondTouchSlop = (TestTouchSlop + 1).toFloat()
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var event = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(event)
+ // Out of touch slop region
+ event = event.moveBy(Duration(milliseconds = 10), 0f, beyondTouchSlop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(event)
+ // Back into touch slop region
+ event = event.moveBy(Duration(milliseconds = 10), 0f, -beyondTouchSlop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(event)
+ // Out of touch slop region again
+ event = event.moveBy(Duration(milliseconds = 10), 0f, beyondTouchSlop)
+ recognizer.pointerInputHandler.invokeOverAllPasses(event)
+
+ assertThat(log.filter { it.methodName == "onStart" }).hasSize(1)
+ assertThat(log.filter { it.methodName == "onDrag" }).hasSize(3)
+ }
+
+ @Test
+ fun pointerInputHandler_downConsumedMovePassedSlop_onStartAndOnDragCalledOnce() {
+ val beyondTouchSlop = (TestTouchSlop + 1).toFloat()
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+ val down = down().consumeDownChange()
+
+ recognizer.pointerInputHandler.invokeOverAllPasses(down)
+ recognizer.pointerInputHandler.invokeOverAllPasses(
+ down.moveBy(
+ Duration(milliseconds = 100),
+ beyondTouchSlop,
+ 0f
+ )
+ )
+
+ assertThat(log.filter { it.methodName == "onStart" }).hasSize(1)
+ assertThat(log.filter { it.methodName == "onDrag" }).hasSize(1)
+ }
+
+ @Test
+ fun pointerInputHandler_beyondInUnsupportThenBeyondInSupport_onStartAndOnDragCalledOnce() {
+ val doubleTouchSlop = (TestTouchSlop * 2).toFloat()
+ val beyondTouchSlop = (TestTouchSlop + 1).toFloat()
+ recognizer.canDrag = MockCanDrag(arrayOf(Direction.UP), log)
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveBy(
+ Duration(milliseconds = 10),
+ 0f,
+ doubleTouchSlop
+ )
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveBy(
+ Duration(milliseconds = 10),
+ 0f,
+ -beyondTouchSlop
+ )
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(log.filter { it.methodName == "onStart" }).hasSize(1)
+ assertThat(log.filter { it.methodName == "onDrag" }).hasSize(1)
+ }
+
+ // onDrag called with correct values verification
+
+ @Test
+ fun pointerInputHandler_justPassedSlop_onDragCalledWithTotalDistance() {
+ val beyondTouchSlop = (TestTouchSlop + 1).toFloat()
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveBy(
+ Duration(milliseconds = 100),
+ beyondTouchSlop,
+ 0f
+ )
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(log).contains(LogItem("onDrag", pxPosition = PxPosition(11.px, 0.px)))
+ }
+
+ @Test
+ fun pointerInputHandler_moveAndMoveConsumed_onDragCalledWithCorrectDistance() {
+ val beyondTouchSlop = (TestTouchSlop + 1).toFloat()
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveBy(Duration(milliseconds = 100), beyondTouchSlop, 0f)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveBy(Duration(milliseconds = 100), 3f, -5f)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveBy(Duration(milliseconds = 100), -3f, 7f)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveBy(Duration(milliseconds = 100), 11f, 13f)
+ .consumePositionChange(5f, 3f)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveBy(Duration(milliseconds = 100), -13f, -11f)
+ .consumePositionChange(-3f, -5f)
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ val { it.methodName == "onDrag" }
+ assertThat(onDragLog).hasSize(5)
+ assertThat(onDragLog[0].pxPosition).isEqualTo(PxPosition(11.px, 0.px))
+ assertThat(onDragLog[1].pxPosition).isEqualTo(PxPosition(3.px, (-5).px))
+ assertThat(onDragLog[2].pxPosition).isEqualTo(PxPosition((-3).px, 7.px))
+ assertThat(onDragLog[3].pxPosition).isEqualTo(PxPosition(6.px, 10.px))
+ assertThat(onDragLog[4].pxPosition).isEqualTo(PxPosition((-10).px, (-6).px))
+ }
+
+ // onStop not called verification
+
+ @Test
+ fun pointerInputHandler_downMoveWithinSlopUp_onStopNotCalled() {
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveTo(
+ 10L.millisecondsToTimestamp(),
+ TestTouchSlop.toFloat(),
+ TestTouchSlop.toFloat()
+ )
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.up(20L.millisecondsToTimestamp())
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(log.filter { it.methodName == "onStop" }).hasSize(0)
+ }
+
+ @Test
+ fun pointerInputHandler_downMoveBeyondSlop_onStopNotCalled() {
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveTo(
+ 10L.millisecondsToTimestamp(),
+ TestTouchSlop.toFloat() + 1,
+ TestTouchSlop.toFloat()
+ )
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(log.filter { it.methodName == "onStop" }).hasSize(0)
+ }
+
+ // onStop called verification
+
+ @Test
+ fun pointerInputHandler_downMoveBeyondSlopUp_onStopCalledOnce() {
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveTo(
+ 10L.millisecondsToTimestamp(),
+ TestTouchSlop.toFloat() + 1,
+ TestTouchSlop.toFloat()
+ )
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.up(20L.millisecondsToTimestamp())
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(log.filter { it.methodName == "onStop" }).hasSize(1)
+ }
+
+ // onStop called with correct values verification
+
+ @Test
+ fun pointerInputHandler_flingBeyondSlop_onStopCalledWithCorrectVelocity() {
+ pointerInputHandler_flingBeyondSlop_onStopCalledWithCorrectVelocity(0f, 1f, 0f, 100f)
+ pointerInputHandler_flingBeyondSlop_onStopCalledWithCorrectVelocity(0f, -1f, 0f, -100f)
+ pointerInputHandler_flingBeyondSlop_onStopCalledWithCorrectVelocity(1f, 0f, 100f, 0f)
+ pointerInputHandler_flingBeyondSlop_onStopCalledWithCorrectVelocity(-1f, 0f, -100f, 0f)
+
+ pointerInputHandler_flingBeyondSlop_onStopCalledWithCorrectVelocity(1f, 1f, 100f, 100f)
+ pointerInputHandler_flingBeyondSlop_onStopCalledWithCorrectVelocity(-1f, 1f, -100f, 100f)
+ pointerInputHandler_flingBeyondSlop_onStopCalledWithCorrectVelocity(1f, -1f, 100f, -100f)
+ pointerInputHandler_flingBeyondSlop_onStopCalledWithCorrectVelocity(-1f, -1f, -100f, -100f)
+ }
+
+ private fun pointerInputHandler_flingBeyondSlop_onStopCalledWithCorrectVelocity(
+ incrementPerMilliX: Float,
+ incrementPerMilliY: Float,
+ expectedPxPerSecondDx: Float,
+ expectedPxPerSecondDy: Float
+ ) {
+ log = mutableListOf()
+ recognizer = DragGestureRecognizer()
+ recognizer.touchSlop = TestTouchSlop.ipx
+ recognizer.canDrag = MockCanDrag(Direction.values(), log)
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ repeat(11) {
+ change = change.moveBy(
+ Duration(milliseconds = 10),
+ incrementPerMilliX,
+ incrementPerMilliY
+ )
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ }
+
+ change = change.up(20L.millisecondsToTimestamp())
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ val velocity = log.find { it.methodName == "onStop" }!!.pxPosition!!
+ assertThat(velocity.x.value).isWithin(.01f).of(expectedPxPerSecondDx)
+ assertThat(velocity.y.value).isWithin(.01f).of(expectedPxPerSecondDy)
+ }
+
+ // Verification that callbacks occur in the correct order
+
+ @Test
+ fun pointerInputHandler_downMoveBeyondSlopUp_callBacksOccurInCorrectOrder() {
+ recognizer.canDrag = MockCanDrag(arrayOf(Direction.DOWN), log)
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveTo(
+ 10L.millisecondsToTimestamp(),
+ TestTouchSlop.toFloat(),
+ TestTouchSlop.toFloat() + 1
+ )
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.up(20L.millisecondsToTimestamp())
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(log).hasSize(5)
+ assertThat(log[0].methodName).isEqualTo("canDrag")
+ assertThat(log[1].methodName).isEqualTo("canDrag")
+ assertThat(log[2].methodName).isEqualTo("onStart")
+ assertThat(log[3].methodName).isEqualTo("onDrag")
+ assertThat(log[4].methodName).isEqualTo("onStop")
+ }
+
+ // Verification about what events are, or aren't consumed.
+
+ @Test
+ fun pointerInputHandler_down_downNotConsumed() {
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ val result = recognizer.pointerInputHandler.invokeOverAllPasses(down())
+
+ assertThat(result.consumed.downChange).isFalse()
+ }
+
+ @Test
+ fun pointerInputHandler_downMoveWithinTouchSlop_distanceChangeNotConsumed() {
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveTo(
+ 10L.millisecondsToTimestamp(),
+ TestTouchSlop.toFloat(),
+ TestTouchSlop.toFloat()
+ )
+ val result = recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(result.anyPositionChangeConsumed()).isFalse()
+ }
+
+ @Test
+ fun pointerInputHandler_downMoveBeyondSlopInUnsupportedDirection_distanceChangeNotConsumed() {
+ recognizer.canDrag = MockCanDrag(arrayOf(), log)
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveTo(
+ 10L.millisecondsToTimestamp(),
+ TestTouchSlop.toFloat() + 1,
+ TestTouchSlop.toFloat() + 1
+ )
+ val result = recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(result.anyPositionChangeConsumed()).isFalse()
+ }
+
+ @Test
+ fun pointerInputHandler_doneMoveCallBackDoesNotConsume_distanceChangeNotConsumed() {
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveTo(
+ 10L.millisecondsToTimestamp(),
+ TestTouchSlop.toFloat() + 1,
+ TestTouchSlop.toFloat() + 1
+ )
+ val result = recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(result.anyPositionChangeConsumed()).isFalse()
+ }
+
+ @Test
+ fun pointerInputHandler_moveCallBackConsumes_changeDistanceConsumedByCorrectAmount() {
+ val thirdTouchSlop = TestTouchSlop.toFloat() / 3
+ val quarterTouchSlop = TestTouchSlop.toFloat() / 4
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver =
+ MockDragObserver(log, PxPosition(thirdTouchSlop.px, quarterTouchSlop.px))
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveTo(
+ 10L.millisecondsToTimestamp(),
+ TestTouchSlop.toFloat() + 1,
+ TestTouchSlop.toFloat() + 1
+ )
+ val result = recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(result.consumed.positionChange.dx).isEqualTo(thirdTouchSlop)
+ assertThat(result.consumed.positionChange.dy).isEqualTo(quarterTouchSlop)
+ }
+
+ @Test
+ fun pointerInputHandler_onStopConsumesUp() {
+ recognizer.canDrag = canDragMockTrue
+ recognizer.dragObserver = MockDragObserver(log)
+
+ var change = down()
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.moveTo(
+ 10L.millisecondsToTimestamp(),
+ TestTouchSlop.toFloat() + 1,
+ TestTouchSlop.toFloat()
+ )
+ recognizer.pointerInputHandler.invokeOverAllPasses(change)
+ change = change.up(20L.millisecondsToTimestamp())
+ val result = recognizer.pointerInputHandler.invokeOverAllPasses(change)
+
+ assertThat(result.consumed.downChange).isTrue()
+ }
+
+ class MockCanDrag(
+ private val directionsToReturnTrue: Array<Direction>,
+ val log: MutableList<LogItem>
+ ) :
+ (Direction) -> Boolean {
+ override fun invoke(direction: Direction): Boolean {
+ log.add(LogItem("canDrag", direction = direction))
+ return directionsToReturnTrue.contains(direction)
+ }
+ }
+
+ data class LogItem(
+ val methodName: String,
+ val direction: Direction? = null,
+ val pxPosition: PxPosition? = null
+ )
+
+ class MockDragObserver(
+ val log: MutableList<LogItem>,
+ var dragConsume: PxPosition = PxPosition.Origin
+ ) : DragObserver() {
+ override fun onStart() {
+ log.add(LogItem("onStart"))
+ super.onStart()
+ }
+
+ override fun onDrag(dragDistance: PxPosition): PxPosition {
+ log.add(LogItem("onDrag", pxPosition = dragDistance))
+ return dragConsume
+ }
+
+ override fun onStop(velocity: PxPosition) {
+ log.add(LogItem("onStop", pxPosition = velocity))
+ super.onStop(velocity)
+ }
+ }
+}
\ No newline at end of file