| /* |
| * 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. |
| */ |
| |
| @file:OptIn(ExperimentalPointerInput::class) |
| |
| package androidx.ui.core.gesture |
| |
| import androidx.compose.runtime.remember |
| import androidx.ui.core.CustomEvent |
| import androidx.ui.core.CustomEventDispatcher |
| import androidx.ui.core.DensityAmbient |
| import androidx.ui.core.Direction |
| import androidx.ui.core.Modifier |
| import androidx.ui.core.PointerEventPass |
| import androidx.ui.core.PointerInputChange |
| import androidx.ui.core.changedToUpIgnoreConsumed |
| import androidx.ui.core.composed |
| import androidx.ui.core.gesture.scrollorientationlocking.Orientation |
| import androidx.ui.core.gesture.scrollorientationlocking.ScrollOrientationLocker |
| import androidx.ui.core.pointerinput.PointerInputFilter |
| import androidx.ui.core.positionChange |
| import androidx.compose.ui.geometry.Offset |
| import androidx.compose.ui.unit.IntSize |
| import kotlin.math.abs |
| |
| /** |
| * This gesture filter detects when the average distance change of all pointers surpasses the touch |
| * slop. |
| * |
| * The value of touch slop is currently defined internally as the constant [TouchSlop]. |
| * |
| * Note: [canDrag] and [orientation] interact such that [canDrag] will only be called for |
| * [Direction]s that are included in the given [orientation]. |
| * |
| * Note: Changing the value of [orientation] will reset the gesture filter such that it will not |
| * respond to input until new pointers are detected. |
| * |
| * @param onDragSlopExceeded Called when touch slop is exceeded in a supported direction and |
| * orientation. |
| * @param canDrag Set to limit the types of directions under which touch slop can be exceeded. |
| * Return true if you want a drag to be started due to the touch slop being surpassed in the |
| * given [Direction]. If [canDrag] is not provided, touch slop will be able to be exceeded in all |
| * directions. |
| * @param orientation If provided, limits the [Direction]s that scroll slop can be exceeded in to |
| * those that are included in the given orientation and does not consider pointers that are locked |
| * to other orientations. |
| */ |
| fun Modifier.dragSlopExceededGestureFilter( |
| onDragSlopExceeded: () -> Unit, |
| canDrag: ((Direction) -> Boolean)? = null, |
| orientation: Orientation? = null |
| ): Modifier = composed { |
| val touchSlop = with(DensityAmbient.current) { TouchSlop.toPx() } |
| val filter = remember { |
| DragSlopExceededGestureFilter(touchSlop) |
| } |
| filter.onDragSlopExceeded = onDragSlopExceeded |
| filter.setDraggableData(orientation, canDrag) |
| PointerInputModifierImpl(filter) |
| } |
| |
| internal class DragSlopExceededGestureFilter( |
| private val touchSlop: Float |
| ) : PointerInputFilter() { |
| private var dxForPass = 0f |
| private var dyForPass = 0f |
| private var dxUnderSlop = 0f |
| private var dyUnderSlop = 0f |
| private var passedSlop = false |
| |
| private var canDrag: ((Direction) -> Boolean)? = null |
| private var orientation: Orientation? = null |
| |
| var onDragSlopExceeded: () -> Unit = {} |
| |
| lateinit var scrollOrientationLocker: ScrollOrientationLocker |
| lateinit var customEventDispatcher: CustomEventDispatcher |
| |
| fun setDraggableData(orientation: Orientation?, canDrag: ((Direction) -> Boolean)?) { |
| this.orientation = orientation |
| this.canDrag = { direction -> |
| when { |
| orientation == Orientation.Horizontal && direction == Direction.UP -> false |
| orientation == Orientation.Horizontal && direction == Direction.DOWN -> false |
| orientation == Orientation.Vertical && direction == Direction.LEFT -> false |
| orientation == Orientation.Vertical && direction == Direction.RIGHT -> false |
| else -> canDrag?.invoke(direction) ?: true |
| } |
| } |
| } |
| |
| override fun onInit(customEventDispatcher: CustomEventDispatcher) { |
| scrollOrientationLocker = ScrollOrientationLocker(customEventDispatcher) |
| } |
| |
| override fun onPointerInput( |
| changes: List<PointerInputChange>, |
| pass: PointerEventPass, |
| bounds: IntSize |
| ): List<PointerInputChange> { |
| |
| scrollOrientationLocker.onPointerInputSetup(changes, pass) |
| |
| if (pass == PointerEventPass.PostUp || pass == PointerEventPass.PostDown) { |
| |
| // Filter changes for those that we can interact with due to our orientation. |
| val applicableChanges = |
| with(orientation) { |
| if (this != null) { |
| scrollOrientationLocker.getPointersFor(changes, this) |
| } else { |
| changes |
| } |
| } |
| |
| if (!passedSlop) { |
| |
| // Get current average change. |
| val averagePositionChange = getAveragePositionChange(applicableChanges) |
| val dx = averagePositionChange.x |
| val dy = averagePositionChange.y |
| |
| // Track changes during postUp and during postDown. This allows for fancy dragging |
| // due to a parent being dragged and will likely be removed. |
| // TODO(b/157087973): Likely remove this two pass complexity. |
| if (pass == PointerEventPass.PostUp) { |
| dxForPass = dx |
| dyForPass = dy |
| dxUnderSlop += dx |
| dyUnderSlop += dy |
| } else { |
| dxUnderSlop += dx - dxForPass |
| dyUnderSlop += dy - dyForPass |
| } |
| |
| // Map the distance to the direction enum for a call to canDrag. |
| val directionX = averagePositionChange.horizontalDirection() |
| val directionY = averagePositionChange.verticalDirection() |
| |
| val canDragX = directionX != null && canDrag?.invoke(directionX) ?: true |
| val canDragY = directionY != null && canDrag?.invoke(directionY) ?: true |
| |
| val passedSlopX = canDragX && abs(dxUnderSlop) > touchSlop |
| val passedSlopY = canDragY && abs(dyUnderSlop) > touchSlop |
| |
| if (passedSlopX || passedSlopY) { |
| passedSlop = true |
| onDragSlopExceeded.invoke() |
| } else { |
| // If we have passed slop in a direction that we can't drag in, we should reset |
| // our tracking back to zero so that a user doesn't have to later scroll the slop |
| // + the extra distance they scrolled in the wrong direction. |
| if (!canDragX && |
| ((directionX == Direction.LEFT && dxUnderSlop < 0) || |
| (directionX == Direction.RIGHT && dxUnderSlop > 0)) |
| ) { |
| dxUnderSlop = 0f |
| } |
| if (!canDragY && |
| ((directionY == Direction.UP && dyUnderSlop < 0) || |
| (directionY == Direction.DOWN && dyUnderSlop > 0)) |
| ) { |
| dyUnderSlop = 0f |
| } |
| } |
| } |
| |
| if (pass == PointerEventPass.PostDown && |
| changes.all { it.changedToUpIgnoreConsumed() } |
| ) { |
| // On the final pass, check to see if all pointers have changed to up, and if they |
| // have, reset. |
| reset() |
| } |
| } |
| |
| scrollOrientationLocker.onPointerInputTearDown(changes, pass) |
| |
| return changes |
| } |
| |
| override fun onCancel() { |
| scrollOrientationLocker.onCancel() |
| reset() |
| } |
| |
| override fun onCustomEvent(customEvent: CustomEvent, pass: PointerEventPass) { |
| scrollOrientationLocker.onCustomEvent(customEvent, pass) |
| } |
| |
| private fun reset() { |
| passedSlop = false |
| dxForPass = 0f |
| dyForPass = 0f |
| dxUnderSlop = 0f |
| dyUnderSlop = 0f |
| } |
| } |
| |
| /** |
| * Get's the average distance change of all pointers as an Offset. |
| */ |
| private fun getAveragePositionChange(changes: List<PointerInputChange>): Offset { |
| if (changes.isEmpty()) { |
| return Offset.Zero |
| } |
| |
| val sum = changes.fold(Offset.Zero) { sum, change -> |
| sum + change.positionChange() |
| } |
| val sizeAsFloat = changes.size.toFloat() |
| // TODO(b/148980115): Once PxPosition is removed, sum will be an Offset, and this line can |
| // just be straight division. |
| return Offset(sum.x / sizeAsFloat, sum.y / sizeAsFloat) |
| } |
| |
| /** |
| * Maps an [Offset] value to a horizontal [Direction]. |
| */ |
| private fun Offset.horizontalDirection() = |
| when { |
| this.x < 0f -> Direction.LEFT |
| this.x > 0f -> Direction.RIGHT |
| else -> null |
| } |
| |
| /** |
| * Maps a [Offset] value to a vertical [Direction]. |
| */ |
| private fun Offset.verticalDirection() = |
| when { |
| this.y < 0f -> Direction.UP |
| this.y > 0f -> Direction.DOWN |
| else -> null |
| } |