[go: nahoru, domu]

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].