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