[go: nahoru, domu]

blob: bd30cc49e09331778351718c20befc3578fa9247 [file] [log] [blame]
/*
* 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.compose.Composable
import androidx.compose.memo
import androidx.compose.unaryPlus
import androidx.ui.core.PointerEventPass
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.positionChange
import androidx.ui.core.px
import androidx.ui.core.IntPxSize
import androidx.ui.core.PointerInputWrapper
interface DragObserver {
/**
* Override to be notified when a drag has started.
*
* This will be called as soon as the DragGestureDetector is allowed to start (canStartDragging is null or
* returns true) and the average distance the pointers have moved
* are not 0 on both the x and y axes.
*
* @param downPosition The pointer input position of the down event.
*
* @see onDrag
* @see onStop
*/
fun onStart(downPosition: PxPosition) {}
/**
* Override to be notified when a distance has been dragged.
*
* When overridden, return the amount of the [dragDistance] that has been consumed.
*
* Called immediately after [onStart] and for every subsequent pointer movement, as long as the
* movement was enough to constitute a drag (the average movement on the x or y axis is not
* equal to 0).
*
* Note: This may be called multiple times in a single pass and the values should be accumulated
* for each call.
*
* @param dragDistance The distance that has been dragged. Reflects the average drag distance
* of all pointers.
*/
fun onDrag(dragDistance: PxPosition) = PxPosition.Origin
/**
* Override to be notified when a drag has stopped.
*
* This is called once all pointers have stopped interacting with this DragGestureDetector.
*
* Only called if the last call between [onStart] and [onStop] was [onStart].
*/
fun onStop(velocity: PxPosition) {}
}
// TODO(shepshapard): Convert to functional component with effects once effects are ready.
// TODO(shepshapard): Should this calculate the drag distance as the average of all fingers
// (Shep thinks this is better), or should it only track the most recent finger to have
// touched the screen over the detector (this is how Android currently does it)?
// TODO(b/139020678): Probably has shared functionality with other movement based detectors.
/**
* This gesture detector detects dragging in any direction.
*
* Note: By default, this gesture detector only waits for a single pointer to have moved to start
* dragging. It is extremely likely that you don't want to use this gesture detector directly, but
* instead use a drag gesture detector that does wait for some other condition to have occurred
* (such as [TouchSlopDragGestureDetector] which waits for a single pointer to have passed touch
* slop before dragging starts).
*
* Dragging begins when the a single pointer has moved and either [canStartDragging] is null or
* returns true. When dragging begins, [DragObserver.onStart] is called. [DragObserver.onDrag] is
* then continuously called whenever the average movement of all pointers has movement along the x
* or y axis. [DragObserver.onStop] is called when the dragging ends due to all of the pointers no
* longer interacting with the DragGestureDetector (for example, the last pointer has been lifted
* off of the DragGestureDetector).
*
* When multiple pointers are touching the detector, the drag distance is taken as the average of
* all of the pointers.
*
* @param dragObserver The callback interface to report all events related to dragging.
* @param canStartDragging If set, Before dragging is started ([DragObserver.onStart] is called),
* canStartDragging is called to check to see if it is allowed to start.
*/
// TODO(b/129784010): Consider also allowing onStart, onDrag, and onStop to be set individually (instead of all being
// set via DragObserver).
@Composable
fun RawDragGestureDetector(
dragObserver: DragObserver,
canStartDragging: (() -> Boolean)? = null,
children: @Composable() () -> Unit
) {
val recognizer = +memo {
RawDragGestureRecognizer()
}
recognizer.dragObserver = dragObserver
recognizer.canStartDragging = canStartDragging
PointerInputWrapper(pointerInputHandler = recognizer.pointerInputHandler, children = children)
}
internal class RawDragGestureRecognizer {
private val velocityTrackers: MutableMap<Int, VelocityTracker> = mutableMapOf()
private val downPositions: MutableMap<Int, PxPosition> = mutableMapOf()
private var started = false
var canStartDragging: (() -> Boolean)? = null
lateinit var dragObserver: DragObserver
val pointerInputHandler =
{ changes: List<PointerInputChange>, pass: PointerEventPass, _: IntPxSize ->
var changesToReturn = changes
if (pass == PointerEventPass.InitialDown) {
if (started) {
// If we are have started we want to prevent any descendants from reacting to
// any down change.
changesToReturn = changesToReturn.map {
if (it.changedToDown()) {
it.consumeDownChange()
} else {
it
}
}
}
}
if (pass == PointerEventPass.PostUp) {
// Handle up changes, which includes removing individual pointer VelocityTrackers
// and potentially calling onStop().
if (changesToReturn.any { it.changedToUpIgnoreConsumed() }) {
var velocityTracker: VelocityTracker? = null
changesToReturn.forEach {
// 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.
if (it.changedToUp()) {
velocityTracker = velocityTrackers.remove(it.id)
} else if (it.changedToUpIgnoreConsumed()) {
velocityTrackers.remove(it.id)
}
// removing stored down position for the pointer.
if (it.changedToUp()) {
downPositions.remove(it.id)
}
}
if (changesToReturn.all { it.changedToUpIgnoreConsumed() }) {
// All of the pointers are up, so reset and call onStop. If we have a
// velocityTracker at this point, that means at least one of the up events
// was not consumed so we should send velocity for flinging.
if (started) {
val velocity: PxPosition? =
if (velocityTracker != null) {
changesToReturn = changesToReturn.map {
it.consumeDownChange()
}
velocityTracker!!.calculateVelocity().pixelsPerSecond
} else {
null
}
started = false
dragObserver.onStop(velocity ?: PxPosition.Origin)
}
}
}
// For each new pointer that has been added, start tracking information about it.
if (changesToReturn.any { it.changedToDownIgnoreConsumed() }) {
changesToReturn.forEach {
// If a pointer has changed to down, we should start tracking information
// about it.
if (it.changedToDownIgnoreConsumed()) {
velocityTrackers[it.id] = VelocityTracker()
.apply {
addPosition(
it.current.timestamp!!,
it.current.position!!
)
}
downPositions[it.id] = it.current.position!!
}
}
}
}
// This if block is run for both PostUp and PostDown to allow for the detector to
// respond to modified changes after ancestors may have modified them. (This allows
// for things like dragging an ancestor scrolling container, while keeping a pointer on
// a descendant scrolling container, and the descendant scrolling container keeping the
// descendant still.)
if (pass == PointerEventPass.PostUp || pass == PointerEventPass.PostDown) {
var (movedChanges, otherChanges) = changesToReturn.partition {
it.current.down && !it.changedToDownIgnoreConsumed()
}
movedChanges.forEach {
// TODO(shepshapard): handle the case that the pointerTrackingData is null,
// either with an exception or a logged error, or something else.
val velocityTracker = velocityTrackers[it.id]
if (velocityTracker != null) {
// Add information to the velocity tracker only during one pass.
// TODO(shepshapard): VelocityTracker needs to be updated to not accept
// position information, but rather vector information about movement.
if (pass == PointerEventPass.PostUp) {
velocityTracker.addPosition(
it.current.timestamp!!,
it.current.position!!
)
}
}
}
// Check to see if we are already started so we don't have to call canStartDragging again.
val canStart = !started && canStartDragging?.invoke() ?: true
// At this point, check to see if we have started, and if we have, we may
// be calling onDrag and updating change information on the PointerInputChanges.
if (started || canStart) {
var totalDx = 0f
var totalDy = 0f
movedChanges.forEach {
totalDx += it.positionChange().x.value
totalDy += it.positionChange().y.value
}
if (totalDx != 0f || totalDy != 0f) {
// At this point, if we have not started, check to see if we should start
// and if we should, update our state and call onStart().
if (!started && canStart) {
started = true
dragObserver.onStart(downPositions.values.averagePosition())
downPositions.clear()
}
if (started) {
val consumed = dragObserver.onDrag(
PxPosition(
(totalDx / changesToReturn.size).px,
(totalDy / changesToReturn.size).px
)
)
movedChanges = movedChanges.map {
it.consumePositionChange(consumed.x, consumed.y)
}
}
}
}
changesToReturn = movedChanges + otherChanges
}
changesToReturn
}
}
private fun Iterable<PxPosition>.averagePosition(): PxPosition {
var x = 0.px
var y = 0.px
forEach {
x += it.x
y += it.y
}
return PxPosition(x / count(), y / count())
}