| /* |
| * Copyright 2020 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:Suppress("INLINE_CLASS_DEPRECATED", "EXPERIMENTAL_FEATURE_WARNING") |
| |
| package androidx.compose.ui.input.pointer |
| |
| import androidx.compose.runtime.Immutable |
| import androidx.compose.ui.Modifier |
| import androidx.compose.ui.geometry.Offset |
| import androidx.compose.ui.input.pointer.PointerEventPass.Final |
| import androidx.compose.ui.input.pointer.PointerEventPass.Initial |
| import androidx.compose.ui.input.pointer.PointerEventPass.Main |
| import androidx.compose.ui.layout.LayoutCoordinates |
| import androidx.compose.ui.node.InternalCoreApi |
| import androidx.compose.ui.unit.IntSize |
| |
| /** |
| * A [Modifier.Element] that can interact with pointer input. |
| */ |
| interface PointerInputModifier : Modifier.Element { |
| val pointerInputFilter: PointerInputFilter |
| } |
| |
| /** |
| * A PointerInputFilter represents a single entity that receives [PointerInputChange]s), |
| * interprets them, and consumes the aspects of the changes that it is react to such that other |
| * PointerInputFilters don't also react to them. |
| */ |
| abstract class PointerInputFilter { |
| |
| /** |
| * Invoked when pointers that previously hit this [PointerInputFilter] have changed. |
| * |
| * @param pointerEvent The list of [PointerInputChange]s with positions relative to this |
| * [PointerInputFilter]. |
| * @param pass The [PointerEventPass] in which this function is being called. |
| * @param bounds The width and height associated with this [PointerInputFilter]. |
| * @return The list of [PointerInputChange]s after any aspect of the changes have been consumed. |
| * |
| * @see PointerInputChange |
| * @see PointerEventPass |
| */ |
| abstract fun onPointerEvent( |
| pointerEvent: PointerEvent, |
| pass: PointerEventPass, |
| bounds: IntSize |
| ) |
| |
| /** |
| * Invoked to notify the handler that no more calls to [PointerInputFilter] will be made, until |
| * at least new pointers exist. This can occur for a few reasons: |
| * 1. Android dispatches ACTION_CANCEL to Compose. |
| * 2. This [PointerInputFilter] is no longer associated with a LayoutNode. |
| * 3. This [PointerInputFilter]'s associated LayoutNode is no longer in the composition tree. |
| */ |
| abstract fun onCancel() |
| |
| internal var layoutCoordinates: LayoutCoordinates? = null |
| |
| /** |
| * The layout size assigned to this [PointerInputFilter]. |
| */ |
| val size: IntSize |
| get() = layoutCoordinates?.size ?: IntSize.Zero |
| internal val isAttached: Boolean |
| get() = layoutCoordinates?.isAttached == true |
| } |
| |
| /** |
| * Describes a pointer input change event that has occurred at a particular point in time. |
| */ |
| expect class PointerEvent @OptIn(InternalCoreApi::class) internal constructor( |
| changes: List<PointerInputChange>, |
| internalPointerEvent: InternalPointerEvent? |
| ) { |
| /** |
| * @param changes The changes. |
| */ |
| constructor(changes: List<PointerInputChange>) |
| |
| /** |
| * The changes. |
| */ |
| val changes: List<PointerInputChange> |
| } |
| |
| /** |
| * The device type that produces a [PointerInputChange], such as a mouse or stylus. |
| */ |
| enum class PointerType { |
| /** |
| * An unknown device type or the device type isn't relevant. |
| */ |
| Unknown, |
| |
| /** |
| * Touch (finger) input. |
| */ |
| Touch, |
| |
| /** |
| * A mouse pointer. |
| */ |
| Mouse, |
| |
| /** |
| * A stylus. |
| */ |
| Stylus, |
| |
| /** |
| * An eraser or an inverted stylus. |
| */ |
| Eraser |
| } |
| |
| /** |
| * Describes a change that has occurred for a particular pointer, as well as how much of the change |
| * has been consumed (meaning, used by a node in the UI). |
| * |
| * The [position] represents the position of the pointer relative to the element that |
| * this [PointerInputChange] is being dispatched to. |
| * |
| * The [previousPosition] represents the position of the pointer offset to the current |
| * position of the pointer relative to the screen. |
| * |
| * This means that [position] and [previousPosition] can always be used to understand how |
| * much a pointer has moved relative to an element, even if that element is moving along with the |
| * changes to the pointer. For example, if a pointer touches a 1x1 pixel box in the middle, |
| * [position] will report a position of (0, 0) when dispatched to it. If the next event |
| * moves x position 5 pixels, [position] will report (5, 0) and [previousPosition] will |
| * report (0, 0). If the box moves all 5 pixels, and the next event represents the pointer moving |
| * along the x axis for 5 more pixels, [position] will again report (5, 0) and |
| * [previousPosition] will report (0, 0). |
| * |
| * @param id The unique id of the pointer associated with this [PointerInputChange]. |
| * @param uptimeMillis The time of the current pointer event, in milliseconds. The start (`0`) time |
| * is platform-dependent |
| * @param position The [Offset] of the current pointer event, relative to the containing |
| * element |
| * @param pressed `true` if the pointer event is considered "pressed." For example, finger |
| * touching the screen or a mouse button is pressed [pressed] would be `true`. |
| * @param previousUptimeMillis The [uptimeMillis] of the previous pointer event |
| * @param previousPosition The [Offset] of the previous pointer event, offset to the |
| * [position] and relative to the containing element. |
| * @param previousPressed `true` if the pointer event was considered "pressed." For example , if |
| * a finger was touching the screen or a mouse button was pressed, [previousPressed] would be |
| * `true`. |
| * @param consumed Which aspects of this change have been consumed. |
| * @param type The device type that produced the event, such as [mouse][PointerType.Mouse], |
| * or [touch][PointerType.Touch].git |
| */ |
| @Immutable |
| class PointerInputChange( |
| val id: PointerId, |
| val uptimeMillis: Long, |
| val position: Offset, |
| val pressed: Boolean, |
| val previousUptimeMillis: Long, |
| val previousPosition: Offset, |
| val previousPressed: Boolean, |
| val consumed: ConsumedData, |
| val type: PointerType = PointerType.Touch |
| ) { |
| fun copy( |
| id: PointerId = this.id, |
| currentTime: Long = this.uptimeMillis, |
| currentPosition: Offset = this.position, |
| currentPressed: Boolean = this.pressed, |
| previousTime: Long = this.previousUptimeMillis, |
| previousPosition: Offset = this.previousPosition, |
| previousPressed: Boolean = this.previousPressed, |
| consumed: ConsumedData = this.consumed, |
| type: PointerType = this.type |
| ): PointerInputChange = PointerInputChange( |
| id, |
| currentTime, |
| currentPosition, |
| currentPressed, |
| previousTime, |
| previousPosition, |
| previousPressed, |
| consumed, |
| type |
| ) |
| } |
| |
| /** |
| * An ID for a given pointer. |
| * |
| * @param value The actual value of the id. |
| */ |
| inline class PointerId(val value: Long) |
| |
| /** |
| * Describes what aspects of a change has been consumed. |
| * |
| * @param positionChange True if a position change in this event has been consumed. |
| * @param downChange True if a change to down or up has been consumed. |
| */ |
| class ConsumedData( |
| var positionChange: Boolean = false, |
| var downChange: Boolean = false |
| ) |
| |
| /** |
| * The enumeration of passes where [PointerInputChange] traverses up and down the UI tree. |
| * |
| * PointerInputChanges traverse throw the hierarchy in the following passes: |
| * |
| * 1. [Initial]: Down the tree from ancestor to descendant. |
| * 2. [Main]: Up the tree from descendant to ancestor. |
| * 3. [Final]: Down the tree from ancestor to descendant. |
| * |
| * These passes serve the following purposes: |
| * |
| * 1. Initial: Allows ancestors to consume aspects of [PointerInputChange] before descendants. |
| * This is where, for example, a scroller may block buttons from getting tapped by other fingers |
| * once scrolling has started. |
| * 2. Main: The primary pass where gesture filters should react to and consume aspects of |
| * [PointerInputChange]s. This is the primary path where descendants will interact with |
| * [PointerInputChange]s before parents. This allows for buttons to respond to a tap before a |
| * container of the bottom to respond to a tap. |
| * 3. Final: This pass is where children can learn what aspects of [PointerInputChange]s were |
| * consumed by parents during the [Main] pass. For example, this is how a button determines that |
| * it should no longer respond to fingers lifting off of it because a parent scroller has |
| * consumed movement in a [PointerInputChange]. |
| */ |
| enum class PointerEventPass { |
| Initial, Main, Final |
| } |
| |
| /** |
| * True if this [PointerInputChange] represents a pointer coming in contact with the screen and |
| * that change has not been consumed. |
| */ |
| fun PointerInputChange.changedToDown() = !consumed.downChange && !previousPressed && pressed |
| |
| /** |
| * True if this [PointerInputChange] represents a pointer coming in contact with the screen, whether |
| * or not that change has been consumed. |
| */ |
| fun PointerInputChange.changedToDownIgnoreConsumed() = !previousPressed && pressed |
| |
| /** |
| * True if this [PointerInputChange] represents a pointer breaking contact with the screen and |
| * that change has not been consumed. |
| */ |
| fun PointerInputChange.changedToUp() = !consumed.downChange && previousPressed && !pressed |
| |
| /** |
| * True if this [PointerInputChange] represents a pointer breaking contact with the screen, whether |
| * or not that change has been consumed. |
| */ |
| fun PointerInputChange.changedToUpIgnoreConsumed() = previousPressed && !pressed |
| |
| /** |
| * True if this [PointerInputChange] represents a pointer moving on the screen and some of that |
| * movement has not been consumed. |
| */ |
| fun PointerInputChange.positionChanged() = |
| this.positionChangeInternal(false) != Offset.Companion.Zero |
| |
| /** |
| * True if this [PointerInputChange] represents a pointer moving on the screen ignoring how much |
| * of that movement may have been consumed. |
| */ |
| fun PointerInputChange.positionChangedIgnoreConsumed() = |
| this.positionChangeInternal(true) != Offset.Companion.Zero |
| |
| /** |
| * The distance that the pointer has moved on the screen minus any distance that has been consumed. |
| */ |
| fun PointerInputChange.positionChange() = this.positionChangeInternal(false) |
| |
| /** |
| * The distance that the pointer has moved on the screen, ignoring the fact that it might have |
| * been consumed. |
| */ |
| fun PointerInputChange.positionChangeIgnoreConsumed() = this.positionChangeInternal(true) |
| |
| private fun PointerInputChange.positionChangeInternal(ignoreConsumed: Boolean = false): Offset { |
| val previousPosition = previousPosition |
| val currentPosition = position |
| |
| val offset = currentPosition - previousPosition |
| |
| return if (!ignoreConsumed && consumed.positionChange) Offset.Zero else offset |
| } |
| |
| /** |
| * True if this [PointerInputChange]'s movement has been consumed. |
| */ |
| fun PointerInputChange.positionChangeConsumed() = consumed.positionChange |
| |
| /** |
| * True if any aspect of this [PointerInputChange] has been consumed. |
| */ |
| fun PointerInputChange.anyChangeConsumed() = positionChangeConsumed() || consumed.downChange |
| |
| /** |
| * Consume the up or down change of this [PointerInputChange] if there is an up or down change to |
| * consume. |
| */ |
| fun PointerInputChange.consumeDownChange() { |
| if (pressed != previousPressed) { |
| consumed.downChange = true |
| } |
| } |
| |
| /** |
| * Consume position change if there is any |
| */ |
| fun PointerInputChange.consumePositionChange() { |
| if (positionChange() != Offset.Zero) { |
| consumed.positionChange = true |
| } |
| } |
| |
| /** |
| * Consumes all changes associated with the [PointerInputChange] |
| */ |
| fun PointerInputChange.consumeAllChanges() { |
| this.consumeDownChange() |
| this.consumePositionChange() |
| } |
| |
| /** |
| * Returns `true` if the pointer has moved outside of the region of |
| * `(0, 0, size.width, size.height)` or `false` if the current pointer is up or it is inside the |
| * given bounds. |
| */ |
| fun PointerInputChange.isOutOfBounds(size: IntSize): Boolean { |
| val position = position |
| val x = position.x |
| val y = position.y |
| val width = size.width |
| val height = size.height |
| return x < 0f || x > width || y < 0f || y > height |
| } |