1 /* <lambda>null2 * Copyright (C) 2023 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 com.android.compose.animation.scene 18 19 import androidx.compose.foundation.gestures.Orientation 20 import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation 21 import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation 22 import androidx.compose.runtime.Stable 23 import androidx.compose.ui.Modifier 24 import androidx.compose.ui.geometry.Offset 25 import androidx.compose.ui.input.pointer.AwaitPointerEventScope 26 import androidx.compose.ui.input.pointer.PointerEvent 27 import androidx.compose.ui.input.pointer.PointerEventPass 28 import androidx.compose.ui.input.pointer.PointerId 29 import androidx.compose.ui.input.pointer.PointerInputChange 30 import androidx.compose.ui.input.pointer.PointerInputScope 31 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode 32 import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed 33 import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed 34 import androidx.compose.ui.input.pointer.positionChange 35 import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed 36 import androidx.compose.ui.input.pointer.util.VelocityTracker 37 import androidx.compose.ui.input.pointer.util.addPointerInputChange 38 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode 39 import androidx.compose.ui.node.DelegatingNode 40 import androidx.compose.ui.node.ModifierNodeElement 41 import androidx.compose.ui.node.ObserverModifierNode 42 import androidx.compose.ui.node.PointerInputModifierNode 43 import androidx.compose.ui.node.currentValueOf 44 import androidx.compose.ui.node.observeReads 45 import androidx.compose.ui.platform.LocalViewConfiguration 46 import androidx.compose.ui.unit.IntSize 47 import androidx.compose.ui.unit.Velocity 48 import androidx.compose.ui.util.fastAll 49 import androidx.compose.ui.util.fastAny 50 import androidx.compose.ui.util.fastFirstOrNull 51 import androidx.compose.ui.util.fastForEach 52 import kotlin.coroutines.cancellation.CancellationException 53 import kotlin.math.sign 54 import kotlinx.coroutines.coroutineScope 55 import kotlinx.coroutines.isActive 56 57 /** 58 * Make an element draggable in the given [orientation]. 59 * 60 * The main difference with [multiPointerDraggable] and 61 * [androidx.compose.foundation.gestures.draggable] is that [onDragStarted] also receives the number 62 * of pointers that are down when the drag is started. If you don't need this information, you 63 * should use `draggable` instead. 64 * 65 * Note that the current implementation is trivial: we wait for the touch slope on the *first* down 66 * pointer, then we count the number of distinct pointers that are down right before calling 67 * [onDragStarted]. This means that the drag won't start when a first pointer is down (but not 68 * dragged) and a second pointer is down and dragged. This is an implementation detail that might 69 * change in the future. 70 */ 71 @Stable 72 internal fun Modifier.multiPointerDraggable( 73 orientation: Orientation, 74 enabled: () -> Boolean, 75 startDragImmediately: (startedPosition: Offset) -> Boolean, 76 onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, 77 swipeDetector: SwipeDetector = DefaultSwipeDetector, 78 ): Modifier = 79 this.then( 80 MultiPointerDraggableElement( 81 orientation, 82 enabled, 83 startDragImmediately, 84 onDragStarted, 85 swipeDetector, 86 ) 87 ) 88 89 private data class MultiPointerDraggableElement( 90 private val orientation: Orientation, 91 private val enabled: () -> Boolean, 92 private val startDragImmediately: (startedPosition: Offset) -> Boolean, 93 private val onDragStarted: 94 (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, 95 private val swipeDetector: SwipeDetector, 96 ) : ModifierNodeElement<MultiPointerDraggableNode>() { 97 override fun create(): MultiPointerDraggableNode = 98 MultiPointerDraggableNode( 99 orientation = orientation, 100 enabled = enabled, 101 startDragImmediately = startDragImmediately, 102 onDragStarted = onDragStarted, 103 swipeDetector = swipeDetector, 104 ) 105 106 override fun update(node: MultiPointerDraggableNode) { 107 node.orientation = orientation 108 node.enabled = enabled 109 node.startDragImmediately = startDragImmediately 110 node.onDragStarted = onDragStarted 111 node.swipeDetector = swipeDetector 112 } 113 } 114 115 internal class MultiPointerDraggableNode( 116 orientation: Orientation, 117 enabled: () -> Boolean, 118 var startDragImmediately: (startedPosition: Offset) -> Boolean, 119 var onDragStarted: 120 (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, 121 var swipeDetector: SwipeDetector = DefaultSwipeDetector, 122 ) : 123 PointerInputModifierNode, 124 DelegatingNode(), 125 CompositionLocalConsumerModifierNode, 126 ObserverModifierNode { <lambda>null127 private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() } 128 private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler)) 129 private val velocityTracker = VelocityTracker() 130 private var previousEnabled: Boolean = false 131 132 var enabled: () -> Boolean = enabled 133 set(value) { 134 // Reset the pointer input whenever enabled changed. 135 if (value != field) { 136 field = value 137 delegate.resetPointerInputHandler() 138 } 139 } 140 141 var orientation: Orientation = orientation 142 set(value) { 143 // Reset the pointer input whenever orientation changed. 144 if (value != field) { 145 field = value 146 delegate.resetPointerInputHandler() 147 } 148 } 149 onAttachnull150 override fun onAttach() { 151 previousEnabled = enabled() 152 onObservedReadsChanged() 153 } 154 onObservedReadsChangednull155 override fun onObservedReadsChanged() { 156 observeReads { 157 val newEnabled = enabled() 158 if (newEnabled != previousEnabled) { 159 delegate.resetPointerInputHandler() 160 } 161 previousEnabled = newEnabled 162 } 163 } 164 onCancelPointerInputnull165 override fun onCancelPointerInput() = delegate.onCancelPointerInput() 166 167 override fun onPointerEvent( 168 pointerEvent: PointerEvent, 169 pass: PointerEventPass, 170 bounds: IntSize 171 ) = delegate.onPointerEvent(pointerEvent, pass, bounds) 172 173 private suspend fun PointerInputScope.pointerInput() { 174 if (!enabled()) { 175 return 176 } 177 178 coroutineScope { 179 awaitPointerEventScope { 180 while (isActive) { 181 try { 182 detectDragGestures( 183 orientation = orientation, 184 startDragImmediately = startDragImmediately, 185 onDragStart = { startedPosition, overSlop, pointersDown -> 186 velocityTracker.resetTracking() 187 onDragStarted(startedPosition, overSlop, pointersDown) 188 }, 189 onDrag = { controller, change, amount -> 190 velocityTracker.addPointerInputChange(change) 191 controller.onDrag(amount) 192 }, 193 onDragEnd = { controller -> 194 val viewConfiguration = currentValueOf(LocalViewConfiguration) 195 val maxVelocity = 196 viewConfiguration.maximumFlingVelocity.let { Velocity(it, it) } 197 val velocity = velocityTracker.calculateVelocity(maxVelocity) 198 controller.onStop( 199 velocity = 200 when (orientation) { 201 Orientation.Horizontal -> velocity.x 202 Orientation.Vertical -> velocity.y 203 }, 204 canChangeScene = true, 205 ) 206 }, 207 onDragCancel = { controller -> 208 controller.onStop(velocity = 0f, canChangeScene = true) 209 }, 210 swipeDetector = swipeDetector 211 ) 212 } catch (exception: CancellationException) { 213 // If the coroutine scope is active, we can just restart the drag cycle. 214 if (!isActive) { 215 throw exception 216 } 217 } 218 } 219 } 220 } 221 } 222 223 /** 224 * Detect drag gestures in the given [orientation]. 225 * 226 * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and 227 * [androidx.compose.foundation.gestures.detectVerticalDragGestures] to add support for: 228 * 1) starting the gesture immediately without requiring a drag >= touch slope; 229 * 2) passing the number of pointers down to [onDragStart]. 230 */ detectDragGesturesnull231 private suspend fun AwaitPointerEventScope.detectDragGestures( 232 orientation: Orientation, 233 startDragImmediately: (startedPosition: Offset) -> Boolean, 234 onDragStart: 235 (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, 236 onDrag: (controller: DragController, change: PointerInputChange, dragAmount: Float) -> Unit, 237 onDragEnd: (controller: DragController) -> Unit, 238 onDragCancel: (controller: DragController) -> Unit, 239 swipeDetector: SwipeDetector, 240 ) { 241 val consumablePointer = 242 awaitConsumableEvent { 243 // We are searching for an event that can be used as the starting point for the 244 // drag gesture. Our options are: 245 // - Initial: These events should never be consumed by the MultiPointerDraggable 246 // since our ancestors can consume the gesture, but we would eliminate this 247 // possibility for our descendants. 248 // - Main: These events are consumed during the drag gesture, and they are a 249 // good place to start if the previous event has not been consumed. 250 // - Final: If the previous event has been consumed, we can wait for the Main 251 // pass to finish. If none of our ancestors were interested in the event, we 252 // can wait for an unconsumed event in the Final pass. 253 val previousConsumed = currentEvent.changes.fastAny { it.isConsumed } 254 if (previousConsumed) PointerEventPass.Final else PointerEventPass.Main 255 } 256 .changes 257 .first() 258 259 var overSlop = 0f 260 val drag = 261 if (startDragImmediately(consumablePointer.position)) { 262 consumablePointer.consume() 263 consumablePointer 264 } else { 265 val onSlopReached = { change: PointerInputChange, over: Float -> 266 if (swipeDetector.detectSwipe(change)) { 267 change.consume() 268 overSlop = over 269 } 270 } 271 272 // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once it 273 // is public. 274 val drag = 275 when (orientation) { 276 Orientation.Horizontal -> 277 awaitHorizontalTouchSlopOrCancellation( 278 consumablePointer.id, 279 onSlopReached 280 ) 281 Orientation.Vertical -> 282 awaitVerticalTouchSlopOrCancellation( 283 consumablePointer.id, 284 onSlopReached 285 ) 286 } 287 288 // Make sure that overSlop is not 0f. This can happen when the user drags by exactly 289 // the touch slop. However, the overSlop we pass to onDragStarted() is used to 290 // compute the direction we are dragging in, so overSlop should never be 0f unless 291 // we intercept an ongoing swipe transition (i.e. startDragImmediately() returned 292 // true). 293 if (drag != null && overSlop == 0f) { 294 val delta = (drag.position - consumablePointer.position).toFloat() 295 check(delta != 0f) { "delta is equal to 0" } 296 overSlop = delta.sign 297 } 298 drag 299 } 300 301 if (drag != null) { 302 // Count the number of pressed pointers. 303 val pressed = mutableSetOf<PointerId>() 304 currentEvent.changes.fastForEach { change -> 305 if (change.pressed) { 306 pressed.add(change.id) 307 } 308 } 309 310 val controller = onDragStart(drag.position, overSlop, pressed.size) 311 312 val successful: Boolean 313 try { 314 onDrag(controller, drag, overSlop) 315 316 successful = 317 drag( 318 initialPointerId = drag.id, 319 hasDragged = { it.positionChangeIgnoreConsumed().toFloat() != 0f }, 320 onDrag = { 321 onDrag(controller, it, it.positionChange().toFloat()) 322 it.consume() 323 }, 324 onIgnoredEvent = { 325 // We are still dragging an object, but this event is not of interest to 326 // the caller. 327 // This event will not trigger the onDrag event, but we will consume the 328 // event to prevent another pointerInput from interrupting the current 329 // gesture just because the event was ignored. 330 it.consume() 331 }, 332 ) 333 } catch (t: Throwable) { 334 onDragCancel(controller) 335 throw t 336 } 337 338 if (successful) { 339 onDragEnd(controller) 340 } else { 341 onDragCancel(controller) 342 } 343 } 344 } 345 awaitConsumableEventnull346 private suspend fun AwaitPointerEventScope.awaitConsumableEvent( 347 pass: () -> PointerEventPass, 348 ): PointerEvent { 349 fun canBeConsumed(changes: List<PointerInputChange>): Boolean { 350 // All pointers must be: 351 return changes.fastAll { 352 // A) recently pressed: even if the event has already been consumed, we can still 353 // use the recently added finger event to determine whether to initiate dragging the 354 // scene. 355 it.changedToDownIgnoreConsumed() || 356 // B) unconsumed AND in a new position (on the current axis) 357 it.positionChange().toFloat() != 0f 358 } 359 } 360 361 var event: PointerEvent 362 do { 363 event = awaitPointerEvent(pass = pass()) 364 } while (!canBeConsumed(event.changes)) 365 366 // We found a consumable event in the Main pass 367 return event 368 } 369 toFloatnull370 private fun Offset.toFloat(): Float { 371 return when (orientation) { 372 Orientation.Vertical -> y 373 Orientation.Horizontal -> x 374 } 375 } 376 377 /** 378 * Continues to read drag events until all pointers are up or the drag event is canceled. The 379 * initial pointer to use for driving the drag is [initialPointerId]. [hasDragged] passes the 380 * result whether a change was detected from the drag function or not. 381 * 382 * Whenever the pointer moves, if [hasDragged] returns true, [onDrag] is called; otherwise, 383 * [onIgnoredEvent] is called. 384 * 385 * @return true when gesture ended with all pointers up and false when the gesture was canceled. 386 * 387 * Note: Inspired by DragGestureDetector.kt 388 */ dragnull389 private suspend inline fun AwaitPointerEventScope.drag( 390 initialPointerId: PointerId, 391 hasDragged: (PointerInputChange) -> Boolean, 392 onDrag: (PointerInputChange) -> Unit, 393 onIgnoredEvent: (PointerInputChange) -> Unit, 394 ): Boolean { 395 val pointer = currentEvent.changes.fastFirstOrNull { it.id == initialPointerId } 396 val isPointerUp = pointer?.pressed != true 397 if (isPointerUp) { 398 return false // The pointer has already been lifted, so the gesture is canceled 399 } 400 var pointerId = initialPointerId 401 while (true) { 402 val change = awaitDragOrUp(pointerId, hasDragged, onIgnoredEvent) ?: return false 403 404 if (change.isConsumed) { 405 return false 406 } 407 408 if (change.changedToUpIgnoreConsumed()) { 409 return true 410 } 411 412 onDrag(change) 413 pointerId = change.id 414 } 415 } 416 417 /** 418 * Waits for a single drag in one axis, final pointer up, or all pointers are up. When 419 * [initialPointerId] has lifted, another pointer that is down is chosen to be the finger 420 * governing the drag. When the final pointer is lifted, that [PointerInputChange] is returned. 421 * When a drag is detected, that [PointerInputChange] is returned. A drag is only detected when 422 * [hasDragged] returns `true`. Events that should not be captured are passed to 423 * [onIgnoredEvent]. 424 * 425 * `null` is returned if there was an error in the pointer input stream and the pointer that was 426 * down was dropped before the 'up' was received. 427 * 428 * Note: Inspired by DragGestureDetector.kt 429 */ awaitDragOrUpnull430 private suspend inline fun AwaitPointerEventScope.awaitDragOrUp( 431 initialPointerId: PointerId, 432 hasDragged: (PointerInputChange) -> Boolean, 433 onIgnoredEvent: (PointerInputChange) -> Unit, 434 ): PointerInputChange? { 435 var pointerId = initialPointerId 436 while (true) { 437 val event = awaitPointerEvent() 438 val dragEvent = event.changes.fastFirstOrNull { it.id == pointerId } ?: return null 439 if (dragEvent.changedToUpIgnoreConsumed()) { 440 val otherDown = event.changes.fastFirstOrNull { it.pressed } 441 if (otherDown == null) { 442 // This is the last "up" 443 return dragEvent 444 } else { 445 pointerId = otherDown.id 446 } 447 } else if (hasDragged(dragEvent)) { 448 return dragEvent 449 } else { 450 onIgnoredEvent(dragEvent) 451 } 452 } 453 } 454 } 455