1 /* <lambda>null2 * Copyright 2020 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.compose.ui.input.pointer 18 19 import androidx.collection.LongSparseArray 20 import androidx.collection.MutableLongObjectMap 21 import androidx.collection.MutableObjectList 22 import androidx.collection.mutableObjectListOf 23 import androidx.compose.runtime.collection.MutableVector 24 import androidx.compose.runtime.collection.mutableVectorOf 25 import androidx.compose.ui.ComposeUiFlags 26 import androidx.compose.ui.ExperimentalComposeUiApi 27 import androidx.compose.ui.Modifier 28 import androidx.compose.ui.input.pointer.util.PointerIdArray 29 import androidx.compose.ui.layout.LayoutCoordinates 30 import androidx.compose.ui.node.InternalCoreApi 31 import androidx.compose.ui.node.Nodes 32 import androidx.compose.ui.node.dispatchForKind 33 import androidx.compose.ui.node.layoutCoordinates 34 import androidx.compose.ui.util.fastFirstOrNull 35 import androidx.compose.ui.util.fastForEach 36 37 /** 38 * Organizes pointers and the [PointerInputFilter]s that they hit into a hierarchy such that 39 * [PointerInputChange]s can be dispatched to the [PointerInputFilter]s in a hierarchical fashion. 40 * 41 * @property rootCoordinates the root [LayoutCoordinates] that [PointerInputChange]s will be 42 * relative to. 43 */ 44 @OptIn(ExperimentalComposeUiApi::class) 45 internal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) { 46 47 private var dispatchingEvent = false 48 private var dispatchCancelAfterDispatchedEvent = false 49 private var clearNodeCacheAfterDispatchedEvent = false 50 private var removeSpecificNodesAfterDispatchedEvent = false 51 52 private val nodesToRemove = MutableObjectList<Modifier.Node>() 53 54 /*@VisibleForTesting*/ 55 internal val root: NodeParent = NodeParent() 56 57 private val hitPointerIdsAndNodes = MutableLongObjectMap<MutableObjectList<Node>>(10) 58 59 init { 60 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 61 println("POINTER_INPUT_DEBUG_LOG_TAG HitPathTracker.init()") 62 } 63 } 64 65 /** 66 * Associates a [pointerId] to a list of hit [pointerInputNodes] and keeps track of them. 67 * 68 * This enables future calls to [dispatchChanges] to dispatch the correct [PointerInputChange]s 69 * to the right [PointerInputFilter]s at the right time. 70 * 71 * If [pointerInputNodes] is empty, nothing will be added. 72 * 73 * @param pointerId The id of the pointer that was hit tested against [PointerInputFilter]s 74 * @param pointerInputNodes The [PointerInputFilter]s that were hit by [pointerId]. Must be 75 * ordered from ancestor to descendant. 76 * @param prunePointerIdsAndChangesNotInNodesList Prune [PointerId]s (and associated changes) 77 * that are NOT in the pointerInputNodes parameter from the cached tree of ParentNode/Node. 78 */ 79 fun addHitPath( 80 pointerId: PointerId, 81 pointerInputNodes: List<Modifier.Node>, 82 prunePointerIdsAndChangesNotInNodesList: Boolean = false 83 ) { 84 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 85 println("POINTER_INPUT_DEBUG_LOG_TAG HitPathTracker.addHitPath()") 86 } 87 88 var parent: NodeParent = root 89 hitPointerIdsAndNodes.clear() 90 var merging = true 91 92 eachPin@ for (i in pointerInputNodes.indices) { 93 val pointerInputNode = pointerInputNodes[i] 94 95 // Doesn't add nodes that aren't attached 96 if (pointerInputNode.isAttached) { 97 pointerInputNode.detachedListener = { 98 removePointerInputModifierNode(pointerInputNode) 99 } 100 101 if (merging) { 102 val node = parent.children.firstOrNull { it.modifierNode == pointerInputNode } 103 104 if (node != null) { 105 node.markIsIn() 106 node.pointerIds.add(pointerId) 107 108 val mutableObjectList = 109 hitPointerIdsAndNodes.getOrPut(pointerId.value) { 110 mutableObjectListOf() 111 } 112 113 mutableObjectList.add(node) 114 parent = node 115 continue@eachPin 116 } else { 117 merging = false 118 } 119 } 120 // TODO(lmr): i wonder if Node here and PointerInputNode ought to be the same thing? 121 val node = Node(pointerInputNode).apply { pointerIds.add(pointerId) } 122 123 val mutableObjectList = 124 hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() } 125 126 mutableObjectList.add(node) 127 128 parent.children.add(node) 129 parent = node 130 } 131 } 132 133 if (prunePointerIdsAndChangesNotInNodesList) { 134 hitPointerIdsAndNodes.forEach { key, value -> 135 removeInvalidPointerIdsAndChanges(key, value) 136 } 137 } 138 } 139 140 private fun removePointerInputModifierNode(pointerInputNode: Modifier.Node) { 141 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 142 println("POINTER_INPUT_DEBUG_LOG_TAG removePointerInputModifierNode()") 143 println("POINTER_INPUT_DEBUG_LOG_TAG \t\tpointerInputNode: $pointerInputNode") 144 println("POINTER_INPUT_DEBUG_LOG_TAG \t\t$dispatchingEvent: $dispatchingEvent") 145 } 146 147 if (dispatchingEvent) { 148 removeSpecificNodesAfterDispatchedEvent = true 149 nodesToRemove.add(pointerInputNode) 150 return 151 } 152 root.removePointerInputModifierNode(pointerInputNode) 153 } 154 155 // Removes pointers/changes that are not in the latest hit test 156 private fun removeInvalidPointerIdsAndChanges( 157 pointerId: Long, 158 hitNodes: MutableObjectList<Node> 159 ) { 160 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 161 println("POINTER_INPUT_DEBUG_LOG_TAG removeInvalidPointerIdsAndChanges()") 162 } 163 root.removeInvalidPointerIdsAndChanges(pointerId, hitNodes) 164 } 165 166 /** 167 * Dispatches [internalPointerEvent] through the hierarchy. 168 * 169 * @param internalPointerEvent The change to dispatch. 170 * @return whether this event was dispatched to a [PointerInputFilter] 171 */ 172 fun dispatchChanges( 173 internalPointerEvent: InternalPointerEvent, 174 isInBounds: Boolean = true 175 ): Boolean { 176 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 177 println("POINTER_INPUT_DEBUG_LOG_TAG dispatchChanges()") 178 } 179 180 val changed = 181 root.buildCache( 182 internalPointerEvent.changes, 183 rootCoordinates, 184 internalPointerEvent, 185 isInBounds 186 ) 187 if (!changed) { 188 return false 189 } 190 191 // In some rare cases, a cancel or a request to remove a pointer input node might come in 192 // during an event. To avoid problems, we use `dispatchingEvent` to guard against that and 193 // if a cancel or request to remove nodes comes in, we delay it until after the event has 194 // been dispatched. 195 dispatchingEvent = true 196 var dispatchHit = 197 root.dispatchMainEventPass( 198 internalPointerEvent.changes, 199 rootCoordinates, 200 internalPointerEvent, 201 isInBounds 202 ) 203 dispatchHit = root.dispatchFinalEventPass(internalPointerEvent) || dispatchHit 204 dispatchingEvent = false 205 206 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 207 println("POINTER_INPUT_DEBUG_LOG_TAG dispatchChanges() done, starting after calls...") 208 } 209 210 if (removeSpecificNodesAfterDispatchedEvent) { 211 removeSpecificNodesAfterDispatchedEvent = false 212 213 for (i in 0 until nodesToRemove.size) { 214 removePointerInputModifierNode(nodesToRemove[i]) 215 } 216 nodesToRemove.clear() 217 218 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 219 println( 220 "POINTER_INPUT_DEBUG_LOG_TAG dispatchChanges() finished, " + 221 "removeSpecificNodesAfterDispatchedEvent, " + 222 "removePointerInputModifierNode() finished" 223 ) 224 } 225 } 226 227 if (dispatchCancelAfterDispatchedEvent) { 228 dispatchCancelAfterDispatchedEvent = false 229 processCancel() 230 231 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 232 println( 233 "POINTER_INPUT_DEBUG_LOG_TAG dispatchChanges() finished, " + 234 "dispatchCancelAfterDispatchedEvent, processCancel() finished" 235 ) 236 } 237 } 238 239 if (clearNodeCacheAfterDispatchedEvent) { 240 clearNodeCacheAfterDispatchedEvent = false 241 clearPreviouslyHitModifierNodeCache() 242 243 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 244 println( 245 "POINTER_INPUT_DEBUG_LOG_TAG dispatchChanges() finished, " + 246 "clearNodeCacheAfterDispatchedEvent, " + 247 "clearPreviouslyHitModifierNodeCache() finished" 248 ) 249 } 250 } 251 252 return dispatchHit 253 } 254 255 fun clearPreviouslyHitModifierNodeCache() { 256 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 257 println("POINTER_INPUT_DEBUG_LOG_TAG clearPreviouslyHitModifierNodeCache()") 258 println("POINTER_INPUT_DEBUG_LOG_TAG \t\t$dispatchingEvent: $dispatchingEvent") 259 } 260 261 if (clearNodeCacheAfterDispatchedEvent) { 262 clearNodeCacheAfterDispatchedEvent = true 263 return 264 } 265 root.clear() 266 } 267 268 /** 269 * Dispatches cancel events to all tracked [PointerInputFilter]s to notify them that 270 * [PointerInputFilter.onPointerEvent] will not be called again until all pointers have been 271 * removed from the application and then at least one is added again, and removes all tracked 272 * data. 273 */ 274 fun processCancel() { 275 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) { 276 println("POINTER_INPUT_DEBUG_LOG_TAG processCancel()") 277 println("POINTER_INPUT_DEBUG_LOG_TAG \t\t$dispatchingEvent: $dispatchingEvent") 278 } 279 if (dispatchingEvent) { 280 dispatchCancelAfterDispatchedEvent = true 281 return 282 } 283 root.dispatchCancel() 284 clearPreviouslyHitModifierNodeCache() 285 } 286 } 287 288 /** 289 * Represents a parent node in the [HitPathTracker]'s tree. This primarily exists because the tree 290 * necessarily has a root that is very similar to all other nodes, except that it does not track any 291 * pointer or [PointerInputFilter] information. 292 */ 293 /*@VisibleForTesting*/ 294 @OptIn(InternalCoreApi::class) 295 internal open class NodeParent { 296 val children: MutableVector<Node> = mutableVectorOf() 297 298 // Supports removePointerInputModifierNode() function 299 private val removeMatchingPointerInputModifierNodeList = MutableObjectList<NodeParent>(10) 300 buildCachenull301 open fun buildCache( 302 changes: LongSparseArray<PointerInputChange>, 303 parentCoordinates: LayoutCoordinates, 304 internalPointerEvent: InternalPointerEvent, 305 isInBounds: Boolean 306 ): Boolean { 307 var changed = false 308 children.forEach { 309 changed = 310 it.buildCache(changes, parentCoordinates, internalPointerEvent, isInBounds) || 311 changed 312 } 313 return changed 314 } 315 316 /** 317 * Dispatches [changes] down the tree, for the initial and main pass. 318 * 319 * [changes] and other properties needed in all passes should be cached inside this method so 320 * they can be reused in [dispatchFinalEventPass], since the passes happen consecutively. 321 * 322 * @param changes the map containing [PointerInputChange]s that will be dispatched to relevant 323 * [PointerInputFilter]s 324 * @param parentCoordinates the [LayoutCoordinates] the positional information in [changes] is 325 * relative to 326 * @param internalPointerEvent the [InternalPointerEvent] needed to construct [PointerEvent]s 327 */ dispatchMainEventPassnull328 open fun dispatchMainEventPass( 329 changes: LongSparseArray<PointerInputChange>, 330 parentCoordinates: LayoutCoordinates, 331 internalPointerEvent: InternalPointerEvent, 332 isInBounds: Boolean 333 ): Boolean { 334 var dispatched = false 335 children.forEach { 336 dispatched = 337 it.dispatchMainEventPass( 338 changes, 339 parentCoordinates, 340 internalPointerEvent, 341 isInBounds 342 ) || dispatched 343 } 344 return dispatched 345 } 346 347 /** 348 * Dispatches the final event pass down the tree. 349 * 350 * Properties cached in [dispatchMainEventPass] should be reset after this method, to ensure 351 * clean state for a future pass where pointer IDs / positions might be different. 352 */ dispatchFinalEventPassnull353 open fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean { 354 var dispatched = false 355 children.forEach { 356 dispatched = it.dispatchFinalEventPass(internalPointerEvent) || dispatched 357 } 358 cleanUpHits(internalPointerEvent) 359 return dispatched 360 } 361 362 /** Dispatches the cancel event to all child [Node]s. */ dispatchCancelnull363 open fun dispatchCancel() { 364 children.forEach { it.dispatchCancel() } 365 } 366 removePointerInputModifierNodenull367 open fun removePointerInputModifierNode(pointerInputModifierNode: Modifier.Node) { 368 removeMatchingPointerInputModifierNodeList.clear() 369 370 // adds root first 371 removeMatchingPointerInputModifierNodeList.add(this) 372 373 while (removeMatchingPointerInputModifierNodeList.isNotEmpty()) { 374 val parent = 375 removeMatchingPointerInputModifierNodeList.removeAt( 376 removeMatchingPointerInputModifierNodeList.size - 1 377 ) 378 379 var index = 0 380 while (index < parent.children.size) { 381 val child = parent.children[index] 382 383 if (child.modifierNode == pointerInputModifierNode) { 384 parent.children.remove(child) 385 child.dispatchCancel() 386 // TODO(JJW): Break here if we change tree structure so same node can't be in 387 // multiple locations (they can be now). 388 } else { 389 removeMatchingPointerInputModifierNodeList.add(child) 390 index++ 391 } 392 } 393 } 394 } 395 396 /** Removes all child nodes. */ clearnull397 fun clear() { 398 children.clear() 399 } 400 removeInvalidPointerIdsAndChangesnull401 open fun removeInvalidPointerIdsAndChanges( 402 pointerIdValue: Long, 403 hitNodes: MutableObjectList<Node> 404 ) { 405 children.forEach { it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes) } 406 } 407 cleanUpHitsnull408 open fun cleanUpHits(internalPointerEvent: InternalPointerEvent) { 409 for (i in children.lastIndex downTo 0) { 410 val child = children[i] 411 if (child.pointerIds.isEmpty()) { 412 children.removeAt(i) 413 } 414 } 415 } 416 } 417 418 /** 419 * Represents a single Node in the tree that also tracks a [PointerInputFilter] and which pointers 420 * hit it (tracked as [PointerId]s). 421 */ 422 /*@VisibleForTesting*/ 423 @OptIn(InternalCoreApi::class) 424 internal class Node(val modifierNode: Modifier.Node) : NodeParent() { 425 426 // Note: pointerIds are stored in a structure specific to their value type (PointerId). 427 // This structure uses a LongArray internally, which avoids auto-boxing caused by 428 // a more generic collection such as HashMap or MutableVector. 429 val pointerIds = PointerIdArray() 430 431 /** 432 * Cached properties that will be set before the main event pass, and reset after the final 433 * pass. Since we know that these won't change within the entire pass, we don't need to 434 * calculate / create these for each pass / multiple times during a pass. 435 * 436 * @see buildCache 437 * @see clearCache 438 */ 439 private val relevantChanges: LongSparseArray<PointerInputChange> = LongSparseArray(2) 440 private var coordinates: LayoutCoordinates? = null 441 private var pointerEvent: PointerEvent? = null 442 private var wasIn = false 443 private var isIn = true 444 private var hasExited = true 445 removeInvalidPointerIdsAndChangesnull446 override fun removeInvalidPointerIdsAndChanges( 447 pointerIdValue: Long, 448 hitNodes: MutableObjectList<Node> 449 ) { 450 if (this.pointerIds.contains(pointerIdValue)) { 451 if (!hitNodes.contains(this)) { 452 this.pointerIds.remove(pointerIdValue) 453 this.relevantChanges.remove(pointerIdValue) 454 } 455 } 456 457 children.forEach { it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes) } 458 } 459 dispatchMainEventPassnull460 override fun dispatchMainEventPass( 461 changes: LongSparseArray<PointerInputChange>, 462 parentCoordinates: LayoutCoordinates, 463 internalPointerEvent: InternalPointerEvent, 464 isInBounds: Boolean 465 ): Boolean { 466 // TODO(b/158243568): The below dispatching operations may cause the pointerInputFilter to 467 // become detached. Currently, they just no-op if it becomes detached and the detached 468 // pointerInputFilters are removed from being tracked with the next event. I currently 469 // believe they should be detached immediately. Though, it is possible they should be 470 // detached after the conclusion of dispatch (so onCancel isn't called during calls 471 // to onPointerEvent). As a result we guard each successive dispatch with the same check. 472 return dispatchIfNeeded { 473 val event = pointerEvent!! 474 val size = coordinates!!.size 475 476 // Dispatch on the tunneling pass. 477 modifierNode.dispatchForKind(Nodes.PointerInput) { 478 it.onPointerEvent(event, PointerEventPass.Initial, size) 479 } 480 481 // Dispatch to children. 482 if (modifierNode.isAttached) { 483 children.forEach { 484 it.dispatchMainEventPass( 485 // Pass only the already-filtered and position-translated changes down to 486 // children 487 relevantChanges, 488 coordinates!!, 489 internalPointerEvent, 490 isInBounds 491 ) 492 } 493 } 494 495 if (modifierNode.isAttached) { 496 // Dispatch on the bubbling pass. 497 modifierNode.dispatchForKind(Nodes.PointerInput) { 498 it.onPointerEvent(event, PointerEventPass.Main, size) 499 } 500 } 501 } 502 } 503 dispatchFinalEventPassnull504 override fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean { 505 // TODO(b/158243568): The below dispatching operations may cause the pointerInputFilter to 506 // become detached. Currently, they just no-op if it becomes detached and the detached 507 // pointerInputFilters are removed from being tracked with the next event. I currently 508 // believe they should be detached immediately. Though, it is possible they should be 509 // detached after the conclusion of dispatch (so onCancel isn't called during calls 510 // to onPointerEvent). As a result we guard each successive dispatch with the same check. 511 val result = dispatchIfNeeded { 512 val event = pointerEvent!! 513 val size = coordinates!!.size 514 // Dispatch on the tunneling pass. 515 modifierNode.dispatchForKind(Nodes.PointerInput) { 516 it.onPointerEvent(event, PointerEventPass.Final, size) 517 } 518 519 // Dispatch to children. 520 if (modifierNode.isAttached) { 521 children.forEach { it.dispatchFinalEventPass(internalPointerEvent) } 522 } 523 } 524 cleanUpHits(internalPointerEvent) 525 clearCache() 526 return result 527 } 528 529 /** 530 * Calculates cached properties that will be stored in this [Node] for the duration of both 531 * [dispatchMainEventPass] and [dispatchFinalEventPass]. This allows us to avoid repeated work 532 * between passes, and within passes, as these properties won't change during the overall 533 * dispatch. 534 * 535 * @see clearCache 536 */ buildCachenull537 override fun buildCache( 538 changes: LongSparseArray<PointerInputChange>, 539 parentCoordinates: LayoutCoordinates, 540 internalPointerEvent: InternalPointerEvent, 541 isInBounds: Boolean 542 ): Boolean { 543 val childChanged = 544 super.buildCache(changes, parentCoordinates, internalPointerEvent, isInBounds) 545 546 // Avoid future work if we know this node will no-op 547 if (!modifierNode.isAttached) return true 548 549 modifierNode.dispatchForKind(Nodes.PointerInput) { coordinates = it.layoutCoordinates } 550 551 // In some cases, undelegate() may be called and the modifierNode is still attached, but 552 // the [SuspendingPointerInputModifierNode] is no longer associated with it (since there 553 // are no [Nodes.PointerInput] kinds). In those cases, we skip triggering the event 554 // for this Node. 555 if (coordinates == null) return true 556 557 @OptIn(ExperimentalComposeUiApi::class) 558 for (j in 0 until changes.size()) { 559 val keyValue = changes.keyAt(j) 560 val change = changes.valueAt(j) 561 562 if (pointerIds.contains(keyValue)) { 563 val prevPosition = change.previousPosition 564 val currentPosition = change.position 565 566 if (prevPosition.isValid() && currentPosition.isValid()) { 567 // And translate their position relative to the parent coordinates, to give us a 568 // change local to the PointerInputFilter's coordinates 569 val historical = ArrayList<HistoricalChange>(change.historical.size) 570 571 change.historical.fastForEach { 572 val historicalPosition = it.position 573 // In some rare cases, historic data may have an invalid position, that is, 574 // Offset.Unspecified. In those cases, we don't want to include it in the 575 // data returned to the developer because the values are invalid. 576 if (historicalPosition.isValid()) { 577 historical.add( 578 HistoricalChange( 579 it.uptimeMillis, 580 coordinates!!.localPositionOf( 581 parentCoordinates, 582 historicalPosition 583 ), 584 it.originalEventPosition 585 ) 586 ) 587 } 588 } 589 590 relevantChanges.put( 591 keyValue, 592 change.copy( 593 previousPosition = 594 coordinates!!.localPositionOf(parentCoordinates, prevPosition), 595 currentPosition = 596 coordinates!!.localPositionOf(parentCoordinates, currentPosition), 597 historical = historical 598 ) 599 ) 600 } 601 } 602 } 603 604 if (relevantChanges.isEmpty()) { 605 pointerIds.clear() 606 children.clear() 607 return true // not hit 608 } 609 610 // Clean up any pointerIds that weren't dispatched 611 for (i in pointerIds.lastIndex downTo 0) { 612 val pointerId = pointerIds[i] 613 if (!changes.containsKey(pointerId.value)) { 614 pointerIds.removeAt(i) 615 } 616 } 617 618 val changesList = ArrayList<PointerInputChange>(relevantChanges.size()) 619 for (i in 0 until relevantChanges.size()) { 620 changesList.add(relevantChanges.valueAt(i)) 621 } 622 val event = PointerEvent(changesList, internalPointerEvent) 623 624 val activeHoverChange = 625 event.changes.fastFirstOrNull { internalPointerEvent.activeHoverEvent(it.id) } 626 627 if (activeHoverChange != null) { 628 if (!isInBounds) { 629 isIn = false 630 } else if (!isIn && (activeHoverChange.pressed || activeHoverChange.previousPressed)) { 631 // We have to recalculate isIn because we didn't redo hit testing 632 val size = coordinates!!.size 633 @Suppress("DEPRECATION") 634 isIn = !activeHoverChange.isOutOfBounds(size) 635 } 636 if ( 637 isIn != wasIn && 638 (event.type == PointerEventType.Move || 639 event.type == PointerEventType.Enter || 640 event.type == PointerEventType.Exit) 641 ) { 642 event.type = 643 if (isIn) { 644 PointerEventType.Enter 645 } else { 646 PointerEventType.Exit 647 } 648 } else if (event.type == PointerEventType.Enter && wasIn && !hasExited) { 649 event.type = PointerEventType.Move // We already knew that it was in. 650 } else if (event.type == PointerEventType.Exit && isIn && activeHoverChange.pressed) { 651 event.type = PointerEventType.Move // We are still in. 652 } 653 } 654 655 val changed = 656 childChanged || 657 event.type != PointerEventType.Move || 658 hasPositionChanged(pointerEvent, event) 659 pointerEvent = event 660 return changed 661 } 662 hasPositionChangednull663 private fun hasPositionChanged(oldEvent: PointerEvent?, newEvent: PointerEvent): Boolean { 664 if (oldEvent == null || oldEvent.changes.size != newEvent.changes.size) { 665 return true 666 } 667 for (i in 0 until newEvent.changes.size) { 668 val old = oldEvent.changes[i] 669 val current = newEvent.changes[i] 670 if (old.position != current.position) { 671 return true 672 } 673 } 674 return false 675 } 676 677 /** 678 * Resets cached properties in case this node will continue to track different [pointerIds] than 679 * the ones we built the cache for, instead of being removed. 680 * 681 * @see buildCache 682 */ clearCachenull683 private fun clearCache() { 684 relevantChanges.clear() 685 coordinates = null 686 } 687 688 /** 689 * Calls [block] if there are relevant changes, and if [modifierNode] is attached 690 * 691 * @return whether [block] was called 692 */ dispatchIfNeedednull693 private inline fun dispatchIfNeeded(block: () -> Unit): Boolean { 694 // If there are no relevant changes, there is nothing to process so return false. 695 if (relevantChanges.isEmpty()) return false 696 // If the input filter is not attached, avoid dispatching 697 if (!modifierNode.isAttached) return false 698 699 block() 700 701 // We dispatched to at least one pointer input filter so return true. 702 return true 703 } 704 705 // TODO(shepshapard): Should some order of cancel dispatch be guaranteed? I think the answer is 706 // essentially "no", but given that an order can be consistent... maybe we might as well 707 // set an arbitrary standard and stick to it so user expectations are maintained. 708 /** 709 * Does a depth first traversal and invokes [PointerInputFilter.onCancel] during backtracking. 710 */ dispatchCancelnull711 override fun dispatchCancel() { 712 children.forEach { it.dispatchCancel() } 713 modifierNode.dispatchForKind(Nodes.PointerInput) { it.onCancelPointerInput() } 714 } 715 markIsInnull716 fun markIsIn() { 717 isIn = true 718 } 719 cleanUpHitsnull720 override fun cleanUpHits(internalPointerEvent: InternalPointerEvent) { 721 super.cleanUpHits(internalPointerEvent) 722 723 val event = pointerEvent ?: return 724 725 wasIn = isIn 726 727 event.changes.fastForEach { change -> 728 // There are two scenarios where we need to remove the pointerIds: 729 // 1. Pointer is released AND event stream doesn't have an active hover. 730 // 2. Pointer is released AND is released outside the area. 731 val released = !change.pressed 732 val nonHoverEventStream = !internalPointerEvent.activeHoverEvent(change.id) 733 val outsideArea = !isIn 734 735 val removePointerId = (released && nonHoverEventStream) || (released && outsideArea) 736 737 if (removePointerId) { 738 pointerIds.remove(change.id) 739 } 740 } 741 742 isIn = false 743 hasExited = event.type == PointerEventType.Exit 744 } 745 toStringnull746 override fun toString(): String { 747 return "Node(modifierNode=$modifierNode, children=$children, " + "pointerIds=$pointerIds)" 748 } 749 } 750