Improve TouchSlopExceededGestureFilter.
Actually remove it and replace it with
DragSlopExceededGestureFilter which fires
its callback when the averge change in
pointers exceedes the slop.
Fixes: 157057157
Test: ./gradlew ui:ui-core:test
Change-Id: Ia68c1c8ade9020021f3480f9d6f3c055a9cc5b40
diff --git a/ui/ui-core/src/test/java/androidx/ui/core/gesture/DragSlopExceededGestureFilterTest.kt b/ui/ui-core/src/test/java/androidx/ui/core/gesture/DragSlopExceededGestureFilterTest.kt
new file mode 100644
index 0000000..c5e83ab
--- /dev/null
+++ b/ui/ui-core/src/test/java/androidx/ui/core/gesture/DragSlopExceededGestureFilterTest.kt
@@ -0,0 +1,956 @@
+/*
+ * 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.PointerEventPass
+import androidx.ui.core.consumeDownChange
+import androidx.ui.testutils.consume
+import androidx.ui.testutils.down
+import androidx.ui.testutils.invokeOverAllPasses
+import androidx.ui.testutils.invokeOverPasses
+import androidx.ui.testutils.moveBy
+import androidx.ui.testutils.moveTo
+import androidx.ui.testutils.up
+import androidx.ui.unit.Duration
+import androidx.ui.unit.milliseconds
+import androidx.ui.unit.px
+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 filter or canDrag
+
+// Changing this value will break tests that expect the value to be 10.
+private const val TestTouchSlop = 10
+
+@RunWith(JUnit4::class)
+class DragSlopExceededGestureFilterTest {
+
+ private val onDragSlopExceeded: () -> Unit = { onDragSlopExceededCallCount++ }
+ private val canDrag: (Direction) -> Boolean = { direction ->
+ canDragDirections.add(direction)
+ canDragReturn
+ }
+ private var onDragSlopExceededCallCount: Int = 0
+ private var canDragReturn = false
+ private var canDragDirections: MutableList<Direction> = mutableListOf()
+ private lateinit var filter: DragSlopExceededGestureFilter
+
+ private val TinyNum = .01f
+
+ @Before
+ fun setup() {
+ >
+ canDragReturn = true
+ canDragDirections.clear()
+ filter =
+ DragSlopExceededGestureFilter(TestTouchSlop.px)
+ filter.canDrag = canDrag
+ filter.>
+ }
+
+ // Verify the circumstances under which canDrag should not be called.
+
+ @Test
+ fun onPointerInputChanges_down_canDragNotCalled() {
+ filter::onPointerInput.invokeOverAllPasses(down(0))
+ assertThat(canDragDirections).isEmpty()
+ }
+
+ @Test
+ fun onPointerInputChanges_downUp_canDragNotCalled() {
+ val down = down(0, duration = 0.milliseconds)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val up = down.up(10.milliseconds)
+ filter::onPointerInput.invokeOverAllPasses(up)
+
+ assertThat(canDragDirections).isEmpty()
+ }
+
+ @Test
+ fun onPointerInputChanges_downMoveFullyConsumed_canDragNotCalled() {
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(Duration(milliseconds = 10), 3f, 5f).consume(3f, 5f)
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ assertThat(canDragDirections).isEmpty()
+ }
+
+ // Verify the circumstances under which canDrag should be called.
+
+ @Test
+ fun onPointerInputChanges_downMove1Dimension_canDragCalledOnce() {
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(Duration(milliseconds = 10), 3f, 0f)
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ // Twice because while under touch slop, TouchSlopExceededGestureDetector checks during PostUp and PostDown
+ assertThat(canDragDirections).hasSize(2)
+ }
+
+ @Test
+ fun onPointerInputChanges_downMove2Dimensions_canDragCalledTwice() {
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(Duration(milliseconds = 10), 3f, 5f)
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ // 4 times because while under touch slop, TouchSlopExceededGestureDetector checks during PostUp and
+ // PostDown
+ assertThat(canDragDirections).hasSize(4)
+ }
+
+ @Test
+ fun onPointerInputChanges_downMoveOneDimensionPartiallyConsumed_canDragCalledOnce() {
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(Duration(milliseconds = 10), 0f, 5f).consume(0f, 4f)
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ // Twice because while under touch slop, DragGestureDetector checks during PostUp and
+ // PostDown
+ assertThat(canDragDirections).hasSize(2)
+ }
+
+ @Test
+ fun onPointerInputChanges_downMoveTwoDimensionPartiallyConsumed_canDragCalledTwice() {
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(Duration(milliseconds = 10), 3f, 5f).consume(2f, 4f)
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ // 4 times because while under touch slop, DragGestureDetector checks during PostUp and
+ // PostDown
+ assertThat(canDragDirections).hasSize(4)
+ }
+
+ @Test
+ fun onPointerInputChanges_dragPastTouchSlopOneDimensionAndDrag3MoreTimes_canDragCalledOnce() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ var move = down.moveTo(10.milliseconds, 0f, beyondSlop)
+ filter::onPointerInput.invokeOverAllPasses(move)
+ repeat(3) {
+ move = move.moveBy(Duration(milliseconds = 10), 0f, 1f)
+ filter::onPointerInput.invokeOverAllPasses(move)
+ }
+
+ // Once because although DragGestureDetector checks during PostUp and PostDown, slop is
+ // surpassed during PostUp, and thus isn't checked again.
+ assertThat(canDragDirections).hasSize(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_downMoveUnderSlop3Times_canDragCalled3Times() {
+ val thirdSlop = TestTouchSlop / 3
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ var move = down
+ repeat(3) {
+ move = move.moveBy(Duration(milliseconds = 10), 0f, thirdSlop.toFloat())
+ filter::onPointerInput.invokeOverAllPasses(move)
+ }
+
+ // 6 times because while under touch slop, DragGestureDetector checks during PostUp and
+ // PostDown
+ assertThat(canDragDirections).hasSize(6)
+ }
+
+ @Test
+ fun onPointerInputChanges_moveBeyondSlopThenIntoTouchSlopAreaAndOutAgain_canDragCalledOnce() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ var event = down(0)
+ filter::onPointerInput.invokeOverAllPasses(event)
+ // Out of touch slop region
+ event = event.moveBy(Duration(milliseconds = 10), 0f, beyondSlop)
+ filter::onPointerInput.invokeOverAllPasses(event)
+ // Back into touch slop region
+ event = event.moveBy(Duration(milliseconds = 10), 0f, -beyondSlop)
+ filter::onPointerInput.invokeOverAllPasses(event)
+ // Out of touch slop region again
+ event = event.moveBy(Duration(milliseconds = 10), 0f, beyondSlop)
+ filter::onPointerInput.invokeOverAllPasses(event)
+
+ // Once because although DragGestureDetector checks during PostUp and PostDown, slop is
+ // surpassed during PostUp, and thus isn't checked again.
+ assertThat(canDragDirections).hasSize(1)
+ }
+
+ // Verification of correctness of values passed to canDrag.
+
+ @Test
+ fun onPointerInputChanges_canDragCalledWithCorrectDirection() {
+ onPointerInputChanges_canDragCalledWithCorrectDirection(
+ -1f, 0f, arrayOf(Direction.LEFT)
+ )
+ onPointerInputChanges_canDragCalledWithCorrectDirection(
+ 0f, -1f, arrayOf(Direction.UP)
+ )
+ onPointerInputChanges_canDragCalledWithCorrectDirection(
+ 1f, 0f, arrayOf(Direction.RIGHT)
+ )
+ onPointerInputChanges_canDragCalledWithCorrectDirection(
+ 0f, 1f, arrayOf(Direction.DOWN)
+ )
+ onPointerInputChanges_canDragCalledWithCorrectDirection(
+ -1f, -1f, arrayOf(Direction.LEFT, Direction.UP)
+ )
+ onPointerInputChanges_canDragCalledWithCorrectDirection(
+ -1f, 1f, arrayOf(Direction.LEFT, Direction.DOWN)
+ )
+ onPointerInputChanges_canDragCalledWithCorrectDirection(
+ 1f, -1f, arrayOf(Direction.RIGHT, Direction.UP)
+ )
+ onPointerInputChanges_canDragCalledWithCorrectDirection(
+ 1f, 1f, arrayOf(Direction.RIGHT, Direction.DOWN)
+ )
+ }
+
+ private fun onPointerInputChanges_canDragCalledWithCorrectDirection(
+ dx: Float,
+ dy: Float,
+ expectedDirections: Array<Direction>
+ ) {
+ canDragDirections.clear()
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(Duration(milliseconds = 10), dx, dy)
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ // Everything here is twice because DragGestureDetector checks during PostUp and PostDown.
+ assertThat(canDragDirections).hasSize(expectedDirections.size * 2)
+ expectedDirections.forEach { direction ->
+ assertThat(canDragDirections.count { it == direction })
+ .isEqualTo(2)
+ }
+ }
+
+ // Verify the circumstances under which onTouchSlopExceeded 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 onPointerInputChanges_downMoveWithinSlop_onTouchSlopExceededNotCalled() {
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(
+ Duration(milliseconds = 10),
+ TestTouchSlop.toFloat(),
+ TestTouchSlop.toFloat()
+ )
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun onPointerInputChanges_moveBeyondSlopInUnsupportedDirection_onTouchSlopExceededNotCalled() {
+ val beyondSlop = TestTouchSlop + TinyNum
+ canDragReturn = false
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(
+ Duration(milliseconds = 10),
+ beyondSlop,
+ beyondSlop
+ )
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun onPointerInputChanges_moveBeyondSlopButConsumeUnder_onTouchSlopExceededNotCalled() {
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, TestTouchSlop + TinyNum, 0f).consume(dx = 1f)
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun onPointerInputChanges_moveUnderToPostUpThenModOverInOppDir_onTouchSlopExceededNotCalled() {
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, TestTouchSlop.toFloat(), 0f)
+ filter::onPointerInput.invokeOverPasses(
+ listOf(move),
+ listOf(
+ PointerEventPass.InitialDown,
+ PointerEventPass.PreUp,
+ PointerEventPass.PreDown,
+ PointerEventPass.PostUp
+ )
+ )
+ val move2 = move.consume(dx = (TestTouchSlop * 2f + TinyNum))
+ filter::onPointerInput.invokeOverPasses(
+ move2,
+ PointerEventPass.PostDown
+ )
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ // 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 onPointerInputChanges_moveAroundWithinSlop_onTouchSlopExceededNotCalled() {
+ val slop = TestTouchSlop.toFloat()
+
+ var change = down(0)
+ filter::onPointerInput.invokeOverAllPasses(change)
+
+ // Go around the border of the touch slop area
+
+ // To top left
+ change = change.moveTo(10.milliseconds, -slop, -slop)
+ filter::onPointerInput.invokeOverAllPasses(change)
+ // To bottom left
+ change = change.moveTo(20.milliseconds, -slop, slop)
+ filter::onPointerInput.invokeOverAllPasses(change)
+ // To bottom right
+ change = change.moveTo(30.milliseconds, slop, slop)
+ filter::onPointerInput.invokeOverAllPasses(change)
+ // To top right
+ change = change.moveTo(40.milliseconds, slop, -slop)
+ filter::onPointerInput.invokeOverAllPasses(change)
+
+ // Jump from corner to opposite corner and back
+
+ // To bottom left
+ change = change.moveTo(50.milliseconds, -slop, slop)
+ filter::onPointerInput.invokeOverAllPasses(change)
+ // To top right
+ change = change.moveTo(60.milliseconds, slop, -slop)
+ filter::onPointerInput.invokeOverAllPasses(change)
+
+ // Move the other diagonal
+
+ // To top left
+ change = change.moveTo(70.milliseconds, -slop, -slop)
+ filter::onPointerInput.invokeOverAllPasses(change)
+
+ // Jump from corner to opposite corner and back
+
+ // To bottom right
+ change = change.moveTo(80.milliseconds, slop, slop)
+ filter::onPointerInput.invokeOverAllPasses(change)
+ // To top left
+ change = change.moveTo(90.milliseconds, -slop, -slop)
+ filter::onPointerInput.invokeOverAllPasses(change)
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(0)
+ }
+
+ // Verify the circumstances under which onTouchSlopExceeded should be called.
+
+ @Test
+ fun onPointerInputChanges_movePassedSlop_onTouchSlopExceededCallOnce() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(
+ Duration(milliseconds = 100),
+ beyondSlop,
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_movePassedSlopIn2Events_onTouchSlopExceededCallOnce() {
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(
+ Duration(milliseconds = 100),
+ TestTouchSlop.toFloat(),
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(move)
+ val move2 = down.moveBy(
+ Duration(milliseconds = 100),
+ 1f,
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(move2)
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_passSlopThenInSlopAreaThenOut_onTouchSlopExceededCallOnce() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ var event = down(0)
+ filter::onPointerInput.invokeOverAllPasses(event)
+ // Out of touch slop region
+ event = event.moveBy(Duration(milliseconds = 10), 0f, beyondSlop)
+ filter::onPointerInput.invokeOverAllPasses(event)
+ // Back into touch slop region
+ event = event.moveBy(Duration(milliseconds = 10), 0f, -beyondSlop)
+ filter::onPointerInput.invokeOverAllPasses(event)
+ // Out of touch slop region again
+ event = event.moveBy(Duration(milliseconds = 10), 0f, beyondSlop)
+ filter::onPointerInput.invokeOverAllPasses(event)
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_downConsumedMovePassedSlop_onTouchSlopExceededCallOnce() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ val down = down(0).consumeDownChange()
+ filter::onPointerInput.invokeOverAllPasses(down)
+ val move = down.moveBy(Duration(milliseconds = 100), beyondSlop, 0f)
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_beyondInUnsupportThenBeyondInSupport_onTouchSlopExceededCallOnce() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ var change = down(0)
+ filter::onPointerInput.invokeOverAllPasses(change)
+ canDragReturn = false
+ change = change.moveBy(
+ Duration(milliseconds = 10),
+ 0f,
+ beyondSlop
+ )
+ // Sanity check that onTouchSlopExceeded has not been called.
+ assertThat(onDragSlopExceededCallCount).isEqualTo(0)
+
+ canDragReturn = true
+ filter::onPointerInput.invokeOverAllPasses(change)
+ change = change.moveBy(
+ Duration(milliseconds = 10),
+ 0f,
+ -beyondSlop
+ )
+ filter::onPointerInput.invokeOverAllPasses(change)
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_2PointsMoveInOpposite_onTouchSlopExceededNotCalled() {
+
+ // Arrange
+
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ var pointer1 = down(1)
+ var pointer2 = down(2)
+ filter::onPointerInput.invokeOverAllPasses(pointer1, pointer2)
+
+ // Act
+
+ pointer1 = pointer1.moveBy(
+ Duration(milliseconds = 100),
+ beyondSlop,
+ 0f
+ )
+ pointer2 = pointer2.moveBy(
+ Duration(milliseconds = 100),
+ -beyondSlop,
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(pointer1, pointer2)
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun onPointerInputChanges_3PointsMoveAverage0_onDragSlopExceededNotCalled() {
+
+ // Arrange
+
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ val pointers = arrayOf(down(0), down(1), down(2))
+ filter::onPointerInput.invokeOverAllPasses(*pointers)
+
+ // Act
+
+ // These movements average to no movement.
+ pointers[0] =
+ pointers[0].moveBy(
+ Duration(milliseconds = 100),
+ beyondSlop * -1,
+ beyondSlop * -1
+ )
+ pointers[1] =
+ pointers[1].moveBy(
+ Duration(milliseconds = 100),
+ beyondSlop * 1,
+ beyondSlop * -1
+ )
+ pointers[2] =
+ pointers[2].moveBy(
+ Duration(milliseconds = 100),
+ 0f,
+ beyondSlop * 2
+ )
+ filter::onPointerInput.invokeOverAllPasses(*pointers)
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun onPointerInputChanges_2Points1MoveJustBeyondSlop_onDragSlopExceededNotCalled() {
+
+ // Arrange
+
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ var pointer1 = down(0)
+ var pointer2 = down(1)
+ filter::onPointerInput.invokeOverAllPasses(pointer1, pointer2)
+
+ // Act
+
+ // These movements average to no movement.
+
+ pointer1 =
+ pointer1.moveBy(
+ Duration(milliseconds = 100),
+ 0f,
+ 0f
+ )
+ pointer2 =
+ pointer2.moveBy(
+ Duration(milliseconds = 100),
+ beyondSlop * -1,
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(pointer1, pointer2)
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun onPointerInputChanges_2Points1MoveJustUnderTwiceSlop_onDragSlopExceededNotCalled() {
+
+ // Arrange
+
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ var pointer1 = down(0)
+ var pointer2 = down(1)
+ filter::onPointerInput.invokeOverAllPasses(pointer1, pointer2)
+
+ // Act
+
+ // These movements average to no movement.
+
+ pointer1 =
+ pointer1.moveBy(
+ Duration(milliseconds = 100),
+ 0f,
+ 0f
+ )
+ pointer2 =
+ pointer2.moveBy(
+ Duration(milliseconds = 100),
+ beyondSlop * 2 - 1,
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(pointer1, pointer2)
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun onPointerInputChanges_2Points1MoveToTwiceSlop_onDragSlopExceededNotCalled() {
+
+ // Arrange
+
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ var pointer1 = down(0)
+ var pointer2 = down(1)
+ filter::onPointerInput.invokeOverAllPasses(pointer1, pointer2)
+
+ // Act
+
+ // These movements average to no movement.
+
+ pointer1 =
+ pointer1.moveBy(
+ Duration(milliseconds = 100),
+ 0f,
+ 0f
+ )
+ pointer2 =
+ pointer2.moveBy(
+ Duration(milliseconds = 100),
+ beyondSlop * 2,
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(pointer1, pointer2)
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_1PointMovesBeyondSlopAndThenManyTimes_onDragSlopExceededCallOnce() {
+
+ // Arrange
+
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ var pointer = down(0)
+ filter::onPointerInput.invokeOverAllPasses(pointer)
+
+ // Act
+
+ repeat(5) {
+ pointer = pointer.moveBy(100.milliseconds, beyondSlop, beyondSlop)
+ filter::onPointerInput.invokeOverAllPasses(pointer)
+ }
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_1ModifiedToMoveBeyondSlopBeforePostUp_onDragSlopExceededCallOnce() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, 0f, 0f).consume(dx = beyondSlop)
+ filter::onPointerInput.invokeOverPasses(
+ listOf(move),
+ listOf(
+ PointerEventPass.InitialDown,
+ PointerEventPass.PreUp,
+ PointerEventPass.PreDown,
+ PointerEventPass.PostUp
+ )
+ )
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_1ModedToMoveBeyondSlopBeforePostDown_onDragSlopExceededCallOnce() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, 0f, 0f)
+ filter::onPointerInput.invokeOverPasses(
+ listOf(move),
+ listOf(
+ PointerEventPass.InitialDown,
+ PointerEventPass.PreUp,
+ PointerEventPass.PreDown,
+ PointerEventPass.PostUp
+ )
+ )
+
+ val moveConsumed = move.consume(dx = beyondSlop)
+ filter::onPointerInput.invokeOverPasses(
+ moveConsumed,
+ PointerEventPass.PostDown
+ )
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_moveUnderToPostUpThenModOverToPostDown_onDragSlopExceededCallOnce() {
+ val halfSlop = TestTouchSlop / 2
+ val restOfSlopAndBeyond = TestTouchSlop - halfSlop + TinyNum
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, halfSlop.toFloat(), 0f)
+ filter::onPointerInput.invokeOverPasses(
+ listOf(move),
+ listOf(
+ PointerEventPass.InitialDown,
+ PointerEventPass.PreUp,
+ PointerEventPass.PreDown,
+ PointerEventPass.PostUp
+ )
+ )
+
+ val moveConsumed = move.consume(dx = -restOfSlopAndBeyond)
+ filter::onPointerInput.invokeOverPasses(
+ moveConsumed,
+ PointerEventPass.PostDown
+ )
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun onPointerInputChanges_moveBeyondSlopAllPassesUpToPostUp_onDragSlopExceededCallOnce() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, beyondSlop, 0f)
+ filter::onPointerInput.invokeOverPasses(
+ listOf(move),
+ listOf(
+ PointerEventPass.InitialDown,
+ PointerEventPass.PreUp,
+ PointerEventPass.PreDown,
+ PointerEventPass.PostUp
+ )
+ )
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(1)
+ }
+
+ // Verification that TouchSlopExceededGestureDetector does not consume any changes.
+
+ @Test
+ fun onPointerInputChanges_1Down_nothingConsumed() {
+
+ val result = filter::onPointerInput.invokeOverAllPasses(down(0))
+
+ // Assert
+
+ assertThat(result.consumed.downChange).isFalse()
+ assertThat(result.consumed.positionChange.x).isEqualTo(0.px)
+ assertThat(result.consumed.positionChange.y).isEqualTo(0.px)
+ }
+
+ @Test
+ fun onPointerInputChanges_1MoveUnderSlop_nothingConsumed() {
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, TestTouchSlop.toFloat(), TestTouchSlop.toFloat())
+ val result = filter::onPointerInput.invokeOverAllPasses(move)
+
+ // Assert
+
+ assertThat(result.consumed.downChange).isFalse()
+ assertThat(result.consumed.positionChange.x).isEqualTo(0.px)
+ assertThat(result.consumed.positionChange.y).isEqualTo(0.px)
+ }
+
+ @Test
+ fun onPointerInputChanges_1MoveUnderSlopThenUp_nothingConsumed() {
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, TestTouchSlop.toFloat(), TestTouchSlop.toFloat())
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ val up = move.up(20.milliseconds)
+ val result = filter::onPointerInput.invokeOverAllPasses(up)
+
+ // Assert
+
+ assertThat(result.consumed.downChange).isFalse()
+ assertThat(result.consumed.positionChange.x).isEqualTo(0.px)
+ assertThat(result.consumed.positionChange.y).isEqualTo(0.px)
+ }
+
+ @Test
+ fun onPointerInputChanges_1MoveOverSlop_nothingConsumed() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, beyondSlop, beyondSlop)
+ val result = filter::onPointerInput.invokeOverAllPasses(move)
+
+ // Assert
+
+ assertThat(result.consumed.downChange).isFalse()
+ assertThat(result.consumed.positionChange.x).isEqualTo(0.px)
+ assertThat(result.consumed.positionChange.y).isEqualTo(0.px)
+ }
+
+ @Test
+ fun onPointerInputChanges_1MoveOverSlopThenUp_nothingConsumed() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, beyondSlop, beyondSlop)
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ val up = move.up(20.milliseconds)
+ val result = filter::onPointerInput.invokeOverAllPasses(up)
+
+ // Assert
+
+ assertThat(result.consumed.downChange).isFalse()
+ assertThat(result.consumed.positionChange.x).isEqualTo(0.px)
+ assertThat(result.consumed.positionChange.y).isEqualTo(0.px)
+ }
+
+ // Verification that TouchSlopExceededGestureDetector resets after up correctly.
+
+ @Test
+ fun onPointerInputChanges_MoveBeyondUpDownMoveBeyond_onDragSlopExceededCalledTwice() {
+ val beyondSlop = TestTouchSlop + TinyNum
+
+ repeat(2) {
+ val down = down(0)
+ filter::onPointerInput.invokeOverAllPasses(down)
+
+ val move = down.moveBy(10.milliseconds, beyondSlop, 0f)
+ filter::onPointerInput.invokeOverAllPasses(move)
+
+ val up = move.up(20.milliseconds)
+ filter::onPointerInput.invokeOverAllPasses(up)
+ }
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(2)
+ }
+
+ // Verification that cancellation behavior is correct.
+
+ @Test
+ fun onCancel_underSlopCancelUnderSlop_onDragSlopExceededNotCalled() {
+ val underSlop = TestTouchSlop - TinyNum
+
+ // Arrange
+
+ var pointer = down(0, 0.milliseconds, 0f, 0f)
+ filter::onPointerInput.invokeOverAllPasses(pointer)
+
+ pointer = pointer.moveTo(
+ 10.milliseconds,
+ underSlop,
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(pointer)
+
+ // Act
+
+ filter.onCancel()
+
+ pointer = down(0, 0.milliseconds, 0f, 0f)
+ filter::onPointerInput.invokeOverAllPasses(pointer)
+
+ pointer = pointer.moveTo(
+ 10.milliseconds,
+ underSlop,
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(pointer)
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun onCancel_pastSlopCancelPastSlop_onScaleSlopExceededCalledTwice() {
+ val overSlop = TestTouchSlop + TinyNum
+
+ // Arrange
+
+ var pointer = down(0, 0.milliseconds, 0f, 0f)
+ filter::onPointerInput.invokeOverAllPasses(pointer)
+
+ pointer = pointer.moveTo(
+ 10.milliseconds,
+ overSlop,
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(pointer)
+
+ // Act
+
+ filter.onCancel()
+
+ pointer = down(0, 0.milliseconds, 0f, 0f)
+ filter::onPointerInput.invokeOverAllPasses(pointer)
+
+ pointer = pointer.moveTo(
+ 10.milliseconds,
+ overSlop,
+ 0f
+ )
+ filter::onPointerInput.invokeOverAllPasses(pointer)
+
+ // Assert
+
+ assertThat(onDragSlopExceededCallCount).isEqualTo(2)
+ }
+}
\ No newline at end of file