shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2019 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package androidx.ui.core.gesture |
| 18 | |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 19 | import androidx.compose.Composable |
Leland Richardson | 7f848ab | 2019-12-12 13:43:41 -0800 | [diff] [blame] | 20 | import androidx.compose.remember |
George Mount | 842c8c1 | 2020-01-08 16:03:42 -0800 | [diff] [blame] | 21 | import androidx.ui.core.CoroutineContextAmbient |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 22 | import androidx.ui.core.CustomEvent |
| 23 | import androidx.ui.core.CustomEventDispatcher |
Shep Shapard | 8931646 | 2020-03-03 17:00:52 -0800 | [diff] [blame] | 24 | import androidx.ui.core.Modifier |
George Mount | 842c8c1 | 2020-01-08 16:03:42 -0800 | [diff] [blame] | 25 | import androidx.ui.core.PointerEventPass |
Shep Shapard | 8931646 | 2020-03-03 17:00:52 -0800 | [diff] [blame] | 26 | import androidx.ui.core.PointerId |
George Mount | 842c8c1 | 2020-01-08 16:03:42 -0800 | [diff] [blame] | 27 | import androidx.ui.core.PointerInputChange |
George Mount | 842c8c1 | 2020-01-08 16:03:42 -0800 | [diff] [blame] | 28 | import androidx.ui.core.anyPositionChangeConsumed |
| 29 | import androidx.ui.core.changedToDown |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 30 | import androidx.ui.core.changedToUp |
| 31 | import androidx.ui.core.changedToUpIgnoreConsumed |
| 32 | import androidx.ui.core.consumeDownChange |
Shep Shapard | 8931646 | 2020-03-03 17:00:52 -0800 | [diff] [blame] | 33 | import androidx.ui.core.pointerinput.PointerInputFilter |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 34 | import androidx.ui.temputils.delay |
George Mount | 842c8c1 | 2020-01-08 16:03:42 -0800 | [diff] [blame] | 35 | import androidx.ui.unit.IntPxSize |
| 36 | import androidx.ui.unit.PxPosition |
George Mount | 39cbf92 | 2020-03-25 17:32:47 -0700 | [diff] [blame^] | 37 | import androidx.ui.util.fastAny |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 38 | import kotlinx.coroutines.Job |
| 39 | import kotlin.coroutines.CoroutineContext |
| 40 | |
shepshapard | 0cc02c4 | 2019-06-28 15:51:14 -0700 | [diff] [blame] | 41 | // TODO(b/137569202): This bug tracks the note below regarding the need to eventually |
| 42 | // improve LongPressGestureDetector. |
| 43 | // TODO(b/139020678): Probably has shared functionality with other press based detectors. |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 44 | /** |
| 45 | * Responds to a pointer being "down" for an extended amount of time. |
| 46 | * |
shepshapard | 0cc02c4 | 2019-06-28 15:51:14 -0700 | [diff] [blame] | 47 | * Note: this is likely a temporary, naive, and flawed approach. It is not necessarily guaranteed |
| 48 | * to interoperate well with forthcoming behavior related to disambiguation between multi-tap |
| 49 | * (double tap, triple tap) and tap. |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 50 | */ |
| 51 | @Composable |
| 52 | fun LongPressGestureDetector( |
Shep Shapard | 8931646 | 2020-03-03 17:00:52 -0800 | [diff] [blame] | 53 | onLongPress: (PxPosition) -> Unit |
| 54 | ): Modifier { |
Chuck Jazdzewski | fb6db65 | 2019-11-25 08:30:51 -0800 | [diff] [blame] | 55 | val coroutineContext = CoroutineContextAmbient.current |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 56 | val recognizer = remember { LongPressGestureRecognizer(coroutineContext) } |
shepshapard | 485257c | 2019-08-06 17:43:31 -0700 | [diff] [blame] | 57 | recognizer.onLongPress = onLongPress |
Shep Shapard | 02391f8 | 2020-03-18 11:58:21 -0700 | [diff] [blame] | 58 | return PointerInputModifierImpl(recognizer) |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 59 | } |
| 60 | |
| 61 | internal class LongPressGestureRecognizer( |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 62 | private val coroutineContext: CoroutineContext |
Shep Shapard | 8931646 | 2020-03-03 17:00:52 -0800 | [diff] [blame] | 63 | ) : PointerInputFilter() { |
shepshapard | 485257c | 2019-08-06 17:43:31 -0700 | [diff] [blame] | 64 | lateinit var onLongPress: (PxPosition) -> Unit |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 65 | |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 66 | var longPressTimeout = LongPressTimeout |
| 67 | |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 68 | private enum class State { |
| 69 | Idle, Primed, Fired |
| 70 | } |
| 71 | |
| 72 | private var state = State.Idle |
Shep Shapard | 9cfb3338 | 2020-01-16 10:51:53 -0800 | [diff] [blame] | 73 | private val pointerPositions = linkedMapOf<PointerId, PxPosition>() |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 74 | private var job: Job? = null |
| 75 | private lateinit var customEventDispatcher: CustomEventDispatcher |
| 76 | |
Shep Shapard | 8931646 | 2020-03-03 17:00:52 -0800 | [diff] [blame] | 77 | override val initHandler = { customEventDispatcher: CustomEventDispatcher -> |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 78 | this.customEventDispatcher = customEventDispatcher |
| 79 | } |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 80 | |
Shep Shapard | 8931646 | 2020-03-03 17:00:52 -0800 | [diff] [blame] | 81 | override val pointerInputHandler = |
shepshapard | 2f3e116 | 2019-10-18 17:25:22 -0700 | [diff] [blame] | 82 | { changes: List<PointerInputChange>, pass: PointerEventPass, bounds: IntPxSize -> |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 83 | |
| 84 | var changesToReturn = changes |
| 85 | |
| 86 | if (pass == PointerEventPass.InitialDown && state == State.Fired) { |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 87 | // If we fired and have not reset, we should prevent other pointer input nodes from |
| 88 | // responding to up, so consume it early on. |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 89 | changesToReturn = changesToReturn.map { |
| 90 | if (it.changedToUp()) { |
| 91 | it.consumeDownChange() |
| 92 | } else { |
| 93 | it |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | if (pass == PointerEventPass.PostUp) { |
| 99 | if (state == State.Idle && changes.all { it.changedToDown() }) { |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 100 | // If we are idle and all of the changes changed to down, we are prime to fire |
| 101 | // the event. |
| 102 | primeToFire() |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 103 | } else if (state != State.Idle && changes.all { it.changedToUpIgnoreConsumed() }) { |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 104 | // If we have started and all of the changes changed to up, reset to idle. |
| 105 | resetToIdle() |
shepshapard | 2f3e116 | 2019-10-18 17:25:22 -0700 | [diff] [blame] | 106 | } else if (!changesToReturn.anyPointersInBounds(bounds)) { |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 107 | // If all pointers have gone out of bounds, reset to idle. |
| 108 | resetToIdle() |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 109 | } |
| 110 | |
| 111 | if (state == State.Primed) { |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 112 | // If we are primed, keep track of all down pointer positions so we can pass |
| 113 | // pointer position information to the event we will fire. |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 114 | changes.forEach { |
| 115 | if (it.current.down) { |
| 116 | pointerPositions[it.id] = it.current.position!! |
| 117 | } else { |
| 118 | pointerPositions.remove(it.id) |
| 119 | } |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 124 | if ( |
| 125 | pass == PointerEventPass.PostDown && |
shepshapard | d9f0523 | 2019-08-08 10:21:47 -0700 | [diff] [blame] | 126 | state != State.Idle && |
George Mount | 39cbf92 | 2020-03-25 17:32:47 -0700 | [diff] [blame^] | 127 | changes.fastAny { it.anyPositionChangeConsumed() } |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 128 | ) { |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 129 | // If we are anything but Idle and something consumed movement, reset. |
| 130 | resetToIdle() |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 131 | } |
| 132 | |
| 133 | changesToReturn |
| 134 | } |
| 135 | |
Shep Shapard | 8931646 | 2020-03-03 17:00:52 -0800 | [diff] [blame] | 136 | override val customEventHandler: (CustomEvent, PointerEventPass) -> Unit = |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 137 | { customEvent, pointerInputPass -> |
| 138 | if ( |
| 139 | state == State.Primed && |
| 140 | customEvent is LongPressFiredEvent && |
| 141 | pointerInputPass == PointerEventPass.InitialDown |
| 142 | ) { |
| 143 | // If we are primed but something else fired long press, we should reset. |
| 144 | // Doesn't matter what pass we are on, just choosing one so we only reset once. |
| 145 | resetToIdle() |
| 146 | } |
| 147 | } |
| 148 | |
Shep Shapard | 8931646 | 2020-03-03 17:00:52 -0800 | [diff] [blame] | 149 | override val cancelHandler = { |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 150 | resetToIdle() |
shepshapard | c56cc4e | 2019-07-03 14:31:55 -0700 | [diff] [blame] | 151 | } |
Shep Shapard | 2cdd21a | 2020-01-24 13:44:54 -0800 | [diff] [blame] | 152 | |
| 153 | private fun fireLongPress() { |
| 154 | state = State.Fired |
| 155 | onLongPress.invoke(pointerPositions.asIterable().first().value) |
| 156 | customEventDispatcher.dispatchCustomEvent(LongPressFiredEvent) |
| 157 | } |
| 158 | |
| 159 | private fun primeToFire() { |
| 160 | state = State.Primed |
| 161 | job = delay(longPressTimeout, coroutineContext) { |
| 162 | fireLongPress() |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | private fun resetToIdle() { |
| 167 | state = State.Idle |
| 168 | job?.cancel() |
| 169 | pointerPositions.clear() |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | object LongPressFiredEvent : CustomEvent |