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.node 18 19 import androidx.compose.runtime.collection.mutableVectorOf 20 import androidx.compose.ui.internal.checkPrecondition 21 import androidx.compose.ui.internal.requirePrecondition 22 import androidx.compose.ui.layout.OnGloballyPositionedModifier 23 import androidx.compose.ui.node.LayoutNode.LayoutState.Idle 24 import androidx.compose.ui.node.LayoutNode.LayoutState.LayingOut 25 import androidx.compose.ui.node.LayoutNode.LayoutState.LookaheadLayingOut 26 import androidx.compose.ui.node.LayoutNode.LayoutState.LookaheadMeasuring 27 import androidx.compose.ui.node.LayoutNode.LayoutState.Measuring 28 import androidx.compose.ui.node.LayoutNode.UsageByParent.InLayoutBlock 29 import androidx.compose.ui.node.LayoutNode.UsageByParent.InMeasureBlock 30 import androidx.compose.ui.node.RootForTest.UncaughtExceptionHandler 31 import androidx.compose.ui.unit.Constraints 32 33 /** 34 * Keeps track of [LayoutNode]s which needs to be remeasured or relaid out. 35 * 36 * Use [requestRemeasure] to schedule remeasuring or [requestRelayout] to schedule relayout. 37 * 38 * Use [measureAndLayout] to perform scheduled actions and [dispatchOnPositionedCallbacks] to 39 * dispatch [OnGloballyPositionedModifier] callbacks for the nodes affected by the previous 40 * [measureAndLayout] execution. 41 */ 42 internal class MeasureAndLayoutDelegate(private val root: LayoutNode) { 43 /** LayoutNodes that need measure or layout. */ 44 private val relayoutNodes = DepthSortedSetsForDifferentPasses(Owner.enableExtraAssertions) 45 46 /** Whether any LayoutNode needs measure or layout. */ 47 val hasPendingMeasureOrLayout 48 get() = relayoutNodes.isNotEmpty() 49 50 /** Whether any on positioned callbacks need to be dispatched */ 51 val hasPendingOnPositionedCallbacks 52 get() = onPositionedDispatcher.isNotEmpty() 53 54 /** Flag to indicate that we're currently measuring. */ 55 internal var duringMeasureLayout = false 56 /** 57 * True when we are currently executing a full measure/layout pass, which mean we will iterate 58 * through all the nodes in [relayoutNodes]. 59 */ 60 private var duringFullMeasureLayoutPass = false 61 62 /** Dispatches on positioned callbacks. */ 63 private val onPositionedDispatcher = OnPositionedDispatcher() 64 65 /** List of listeners that must be called after layout has completed. */ 66 private val onLayoutCompletedListeners = mutableVectorOf<Owner.OnLayoutCompletedListener>() 67 68 /** 69 * The current measure iteration. The value is incremented during the [measureAndLayout] 70 * execution. Some [measureAndLayout] executions will increment it more than once. 71 */ 72 var measureIteration: Long = 1L 73 get() { 74 requirePrecondition(duringMeasureLayout) { 75 "measureIteration should be only used during the measure/layout pass" 76 } 77 return field 78 } 79 private set 80 81 /** 82 * Stores the list of [LayoutNode]s scheduled to be remeasured in the next measure/layout pass. 83 * We were unable to mark them as needsRemeasure=true previously as this request happened during 84 * the previous measure/layout pass and they were already measured as part of it. See 85 * [requestRemeasure] for more details. 86 */ 87 private val postponedMeasureRequests = mutableVectorOf<PostponedRequest>() 88 89 private var rootConstraints: Constraints? = null 90 91 internal var uncaughtExceptionHandler: UncaughtExceptionHandler? = null 92 93 /** @param constraints The constraints to measure the root [LayoutNode] with */ 94 fun updateRootConstraints(constraints: Constraints) { 95 if (rootConstraints != constraints) { 96 requirePrecondition(!duringMeasureLayout) { 97 "updateRootConstraints called while measuring" 98 } 99 rootConstraints = constraints 100 if (root.lookaheadRoot != null) { 101 root.markLookaheadMeasurePending() 102 } 103 root.markMeasurePending() 104 relayoutNodes.add(root, root.lookaheadRoot != null) 105 } 106 } 107 108 private val consistencyChecker: LayoutTreeConsistencyChecker? = 109 if (Owner.enableExtraAssertions) { 110 LayoutTreeConsistencyChecker( 111 root, 112 relayoutNodes, 113 postponedMeasureRequests.asMutableList(), 114 ) 115 } else { 116 null 117 } 118 119 /** 120 * Requests lookahead remeasure for this [layoutNode] and nodes affected by its measure result 121 * 122 * Note: This should only be called on a [LayoutNode] in the subtree defined in a 123 * LookaheadScope. The caller is responsible for checking with [LayoutNode.lookaheadRoot] is 124 * valid (i.e. non-null) before calling this method. 125 * 126 * @return true if the [measureAndLayout] execution should be scheduled as a result of the 127 * request. 128 */ 129 fun requestLookaheadRemeasure(layoutNode: LayoutNode, forced: Boolean = false): Boolean { 130 checkPrecondition(layoutNode.lookaheadRoot != null) { 131 "Error: requestLookaheadRemeasure cannot be called on a node outside" + 132 " LookaheadScope" 133 } 134 return when (layoutNode.layoutState) { 135 LookaheadMeasuring -> { 136 // requestLookaheadRemeasure has already been called for this node or 137 // we're currently measuring it, let's swallow. 138 false 139 } 140 Measuring, 141 LookaheadLayingOut, 142 LayingOut -> { 143 // requestLookaheadRemeasure is currently laying out and it is incorrect to 144 // request lookahead remeasure now, let's postpone it. 145 postponedMeasureRequests.add( 146 PostponedRequest(node = layoutNode, isLookahead = true, isForced = forced) 147 ) 148 consistencyChecker?.assertConsistent() 149 false 150 } 151 Idle -> { 152 if (layoutNode.lookaheadMeasurePending && !forced) { 153 false 154 } else { 155 layoutNode.markLookaheadMeasurePending() 156 layoutNode.markMeasurePending() 157 // for the deactivated nodes we want to mark them as dirty, but not to trigger 158 // measureAndLayout() pass as they will be skipped. 159 if (layoutNode.isDeactivated) { 160 false 161 } else { 162 if ( 163 (layoutNode.isPlacedInLookahead == true || 164 layoutNode.canAffectParentInLookahead) && 165 layoutNode.parent?.lookaheadMeasurePending != true 166 ) { 167 relayoutNodes.add(layoutNode, true) 168 } else if ( 169 (layoutNode.isPlaced || layoutNode.canAffectPlacedParent) && 170 layoutNode.parent?.measurePending != true 171 ) { 172 relayoutNodes.add(layoutNode, false) 173 } 174 !duringFullMeasureLayoutPass 175 } 176 } 177 } 178 } 179 } 180 181 /** 182 * Requests remeasure for this [layoutNode] and nodes affected by its measure result. 183 * 184 * @return true if the [measureAndLayout] execution should be scheduled as a result of the 185 * request. 186 */ 187 fun requestRemeasure(layoutNode: LayoutNode, forced: Boolean = false): Boolean = 188 when (layoutNode.layoutState) { 189 Measuring, 190 LookaheadMeasuring -> { 191 // requestMeasure has already been called for this node or 192 // we're currently measuring it, let's swallow. example when it happens: we compose 193 // DataNode inside BoxWithConstraints, this calls onRequestMeasure on DataNode's 194 // parent, but this parent is BoxWithConstraints which is currently measuring. 195 false 196 } 197 LookaheadLayingOut, 198 LayingOut -> { 199 // requestMeasure is currently laying out and it is incorrect to request remeasure 200 // now, let's postpone it. 201 postponedMeasureRequests.add( 202 PostponedRequest(node = layoutNode, isLookahead = false, isForced = forced) 203 ) 204 consistencyChecker?.assertConsistent() 205 false 206 } 207 Idle -> { 208 if (layoutNode.measurePending && !forced) { 209 false 210 } else { 211 layoutNode.markMeasurePending() 212 // for the deactivated nodes we want to mark them as dirty, but not to trigger 213 // measureAndLayout() pass as they will be skipped. 214 if (layoutNode.isDeactivated) { 215 false 216 } else { 217 if (layoutNode.isPlaced || layoutNode.canAffectPlacedParent) { 218 if (layoutNode.parent?.measurePending != true) { 219 relayoutNodes.add(layoutNode, false) 220 } 221 !duringFullMeasureLayoutPass 222 } else { 223 false // it can't affect parent 224 } 225 } 226 } 227 } 228 } 229 230 /** 231 * Requests lookahead relayout for this [layoutNode] and nodes affected by its position. 232 * 233 * @return true if the [measureAndLayout] execution should be scheduled as a result of the 234 * request. 235 */ 236 fun requestLookaheadRelayout(layoutNode: LayoutNode, forced: Boolean = false): Boolean = 237 when (layoutNode.layoutState) { 238 LookaheadMeasuring, 239 LookaheadLayingOut -> { 240 // Don't need to do anything else since the parent is already scheduled 241 // for a lookahead relayout (lookahead measure will trigger lookahead 242 // relayout), or lookahead layout is in process right now 243 consistencyChecker?.assertConsistent() 244 false 245 } 246 Measuring, 247 LayingOut, 248 Idle -> { 249 if ( 250 (layoutNode.lookaheadMeasurePending || layoutNode.lookaheadLayoutPending) && 251 !forced 252 ) { 253 // Don't need to do anything else since the parent is already scheduled 254 // for a lookahead relayout (lookahead measure will trigger lookahead 255 // relayout) 256 consistencyChecker?.assertConsistent() 257 false 258 } else { 259 // Mark both lookahead layout and layout as pending, as layout has a 260 // dependency on lookahead layout. 261 layoutNode.markLookaheadLayoutPending() 262 layoutNode.markLayoutPending() 263 // for the deactivated nodes we want to mark them as dirty, but not to trigger 264 // measureAndLayout() pass as they will be skipped. 265 if (layoutNode.isDeactivated) { 266 false 267 } else { 268 val parent = layoutNode.parent 269 if ( 270 layoutNode.isPlacedInLookahead == true && 271 parent?.lookaheadMeasurePending != true && 272 parent?.lookaheadLayoutPending != true 273 ) { 274 relayoutNodes.add(layoutNode, true) 275 } else if ( 276 layoutNode.isPlaced && 277 parent?.layoutPending != true && 278 parent?.measurePending != true 279 ) { 280 relayoutNodes.add(layoutNode, false) 281 } 282 !duringFullMeasureLayoutPass 283 } 284 } 285 } 286 } 287 288 /** 289 * Requests relayout for this [layoutNode] and nodes affected by its position. 290 * 291 * @return true if the [measureAndLayout] execution should be scheduled as a result of the 292 * request. 293 */ 294 fun requestRelayout(layoutNode: LayoutNode, forced: Boolean = false): Boolean = 295 when (layoutNode.layoutState) { 296 Measuring, 297 LookaheadMeasuring, 298 LookaheadLayingOut, 299 LayingOut -> { 300 // don't need to do anything else since the parent is already scheduled 301 // for a relayout (measure will trigger relayout), or is laying out right now 302 consistencyChecker?.assertConsistent() 303 false 304 } 305 Idle -> { 306 val parent = layoutNode.parent 307 val parentIsPlaced = parent == null || parent.isPlaced 308 if ( 309 !forced && 310 (layoutNode.measurePending || 311 (layoutNode.layoutPending && 312 layoutNode.isPlaced == parentIsPlaced && 313 layoutNode.isPlaced == layoutNode.isPlacedByParent)) 314 ) { 315 // don't need to do anything else since the parent is already scheduled 316 // for a relayout (measure will trigger relayout), or is laying out right now 317 consistencyChecker?.assertConsistent() 318 false 319 } else { 320 layoutNode.markLayoutPending() 321 // for the deactivated nodes we want to mark them as dirty, but not to trigger 322 // measureAndLayout() pass as they will be skipped. 323 if (layoutNode.isDeactivated) { 324 false 325 } else { 326 if (layoutNode.isPlacedByParent && parentIsPlaced) { 327 if (parent?.layoutPending != true && parent?.measurePending != true) { 328 relayoutNodes.add(layoutNode, false) 329 } 330 !duringFullMeasureLayoutPass 331 } else { 332 false // the node can't affect parent 333 } 334 } 335 } 336 } 337 } 338 339 /** Request that [layoutNode] and children should call their position change callbacks. */ 340 fun requestOnPositionedCallback(layoutNode: LayoutNode) { 341 onPositionedDispatcher.onNodePositioned(layoutNode) 342 } 343 344 /** @return true if the [LayoutNode] size has been changed. */ 345 private fun doLookaheadRemeasure(layoutNode: LayoutNode, constraints: Constraints?): Boolean { 346 if (layoutNode.lookaheadRoot == null) return false 347 val lookaheadSizeChanged = 348 if (constraints != null) { 349 layoutNode.lookaheadRemeasure(constraints) 350 } else { 351 layoutNode.lookaheadRemeasure() 352 } 353 354 val parent = layoutNode.parent 355 if (lookaheadSizeChanged && parent != null) { 356 if (parent.lookaheadRoot == null) { 357 parent.requestRemeasure(invalidateIntrinsics = false) 358 } else if (layoutNode.measuredByParentInLookahead == InMeasureBlock) { 359 parent.requestLookaheadRemeasure(invalidateIntrinsics = false) 360 } else if (layoutNode.measuredByParentInLookahead == InLayoutBlock) { 361 parent.requestLookaheadRelayout() 362 } 363 } 364 return lookaheadSizeChanged 365 } 366 367 private fun doRemeasure(layoutNode: LayoutNode, constraints: Constraints?): Boolean { 368 val sizeChanged = 369 if (constraints != null) { 370 layoutNode.remeasure(constraints) 371 } else { 372 layoutNode.remeasure() 373 } 374 val parent = layoutNode.parent 375 if (sizeChanged && parent != null) { 376 if (layoutNode.measuredByParent == InMeasureBlock) { 377 parent.requestRemeasure(invalidateIntrinsics = false) 378 } else if (layoutNode.measuredByParent == InLayoutBlock) { 379 parent.requestRelayout() 380 } 381 } 382 return sizeChanged 383 } 384 385 /** 386 * Iterates through all LayoutNodes that have requested layout and measures and lays them out 387 */ 388 fun measureAndLayout(onLayout: (() -> Unit)? = null): Boolean { 389 var rootNodeResized = false 390 performMeasureAndLayout(fullPass = true) { 391 if (relayoutNodes.isNotEmpty()) { 392 relayoutNodes.popEach { layoutNode, affectsLookahead -> 393 val sizeChanged = remeasureAndRelayoutIfNeeded(layoutNode, affectsLookahead) 394 if (layoutNode === root && sizeChanged) { 395 rootNodeResized = true 396 } 397 } 398 onLayout?.invoke() 399 } 400 } 401 callOnLayoutCompletedListeners() 402 return rootNodeResized 403 } 404 405 /** 406 * Only does measurement from the root without doing any placement. This is intended to be 407 * called to determine only how large the root is with minimal effort. 408 */ 409 fun measureOnly() { 410 if (relayoutNodes.isNotEmpty()) { 411 performMeasureAndLayout(fullPass = false) { 412 if (!relayoutNodes.isEmpty(affectsLookahead = true)) { 413 if (root.lookaheadRoot != null) { 414 // This call will walk the tree to look for lookaheadMeasurePending nodes 415 // and 416 // do a lookahead remeasure for those nodes only. 417 remeasureOnly(root, affectsLookahead = true) 418 } else { 419 // First do a lookahead remeasure pass for all the lookaheadMeasurePending 420 // nodes, 421 // followed by a remeasure pass for the rest of the tree. 422 remeasureLookaheadRootsInSubtree(root) 423 } 424 } 425 remeasureOnly(root, affectsLookahead = false) 426 } 427 } 428 } 429 430 private fun remeasureLookaheadRootsInSubtree(layoutNode: LayoutNode) { 431 layoutNode.forEachChild { 432 if (it.measureAffectsParent) { 433 if (it.isOutMostLookaheadRoot) { 434 // This call will walk the subtree to look for lookaheadMeasurePending nodes and 435 // do a recursive lookahead remeasure starting at the root. 436 remeasureOnly(it, affectsLookahead = true) 437 } else { 438 // Only search downward when no lookahead root is found 439 remeasureLookaheadRootsInSubtree(it) 440 } 441 } 442 } 443 } 444 445 fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) { 446 if (layoutNode.isDeactivated) { 447 // regular measureAndLayout() pass will skip deactivated nodes, so here we should 448 // do nothing as well. 449 return 450 } 451 requirePrecondition(layoutNode != root) { "measureAndLayout called on root" } 452 performMeasureAndLayout(fullPass = false) { 453 relayoutNodes.remove(layoutNode) 454 // we don't check for the layoutState as even if the node doesn't need remeasure 455 // it could be remeasured because the constraints changed. 456 val lookaheadSizeChanged = doLookaheadRemeasure(layoutNode, constraints) 457 if ( 458 (lookaheadSizeChanged || layoutNode.lookaheadLayoutPending) && 459 layoutNode.isPlacedInLookahead == true 460 ) { 461 layoutNode.lookaheadReplace() 462 } 463 // Make sure the subtree starting from [layoutNode] are lookahead replaced. This is 464 // needed because the child nodes that are skipped in `lookaheadReplace` above 465 // due to not changing position may not be skipped by the `replace` call below. Hence 466 // we can avoid having `replace` called for nodes that have not been lookahead placed. 467 ensureSubtreeLookaheadReplaced(layoutNode) 468 469 doRemeasure(layoutNode, constraints) 470 if (layoutNode.layoutPending && layoutNode.isPlaced) { 471 layoutNode.replace() 472 onPositionedDispatcher.onNodePositioned(layoutNode) 473 } 474 475 drainPostponedMeasureRequests() 476 } 477 callOnLayoutCompletedListeners() 478 } 479 480 private fun ensureSubtreeLookaheadReplaced(layoutNode: LayoutNode) { 481 layoutNode.forEachChild { 482 if (it.isPlacedInLookahead == true && !it.isDeactivated) { 483 if (relayoutNodes.contains(it, true)) { 484 // Only replace if invalidation pending 485 it.lookaheadReplace() 486 } 487 ensureSubtreeLookaheadReplaced(it) 488 } 489 } 490 } 491 492 private inline fun performMeasureAndLayout(fullPass: Boolean, block: () -> Unit) { 493 requirePrecondition(root.isAttached) { 494 "performMeasureAndLayout called with unattached root" 495 } 496 requirePrecondition(root.isPlaced) { "performMeasureAndLayout called with unplaced root" } 497 requirePrecondition(!duringMeasureLayout) { 498 "performMeasureAndLayout called during measure layout" 499 } 500 // we don't need to measure any children unless we have the correct root constraints 501 if (rootConstraints != null) { 502 duringMeasureLayout = true 503 duringFullMeasureLayoutPass = fullPass 504 try { 505 block() 506 } catch (t: Throwable) { 507 uncaughtExceptionHandler?.onUncaughtException(t) ?: throw t 508 } finally { 509 duringMeasureLayout = false 510 duringFullMeasureLayoutPass = false 511 } 512 consistencyChecker?.assertConsistent() 513 } 514 } 515 516 fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) { 517 onLayoutCompletedListeners += listener 518 } 519 520 private fun callOnLayoutCompletedListeners() { 521 onLayoutCompletedListeners.forEach { it.onLayoutComplete() } 522 onLayoutCompletedListeners.clear() 523 } 524 525 /** 526 * Does actual remeasure and relayout on the node if it is required. The [layoutNode] should be 527 * already removed from [relayoutNodes] before running it. 528 * 529 * When [affectsLookahead] is false, we'll skip lookahead measure & layout, and only measure and 530 * layout as needed. This is needed because we don't want [forceMeasureTheSubtree] that doesn't 531 * affect lookahead to leak into lookahead and start doing lookahead measure/layout. That would 532 * prevent some of the lookahead remeasure/relayout requests from being properly handled as the 533 * starting node of [forceMeasureTheSubtree] would be in [LayoutNode.LayoutState.Measuring] 534 * until it returns. 535 * 536 * Note, when [affectsLookahead] is true, we will only do lookahead measure and layout. 537 * 538 * @return true if the [LayoutNode] size has been changed. 539 */ 540 private fun remeasureAndRelayoutIfNeeded( 541 layoutNode: LayoutNode, 542 affectsLookahead: Boolean = true, 543 relayoutNeeded: Boolean = true 544 ): Boolean { 545 var sizeChanged = false 546 if (layoutNode.isDeactivated) { 547 // we don't remeasure or relayout deactivated nodes. 548 return false 549 } 550 if ( 551 layoutNode.isPlaced || // the root node doesn't have isPlacedByParent = true 552 layoutNode.isPlacedByParent || 553 layoutNode.canAffectPlacedParent || 554 layoutNode.isPlacedInLookahead == true || 555 layoutNode.canAffectParentInLookahead || 556 layoutNode.alignmentLinesRequired 557 ) { 558 val constraints = if (layoutNode === root) rootConstraints!! else null 559 if (affectsLookahead) { 560 // Only do lookahead invalidation when affectsLookahead is true 561 if (layoutNode.lookaheadMeasurePending) { 562 sizeChanged = doLookaheadRemeasure(layoutNode, constraints) 563 } 564 if (relayoutNeeded) { 565 if ( 566 (sizeChanged || layoutNode.lookaheadLayoutPending) && 567 layoutNode.isPlacedInLookahead == true 568 ) { 569 layoutNode.lookaheadReplace() 570 } 571 } 572 } else { 573 if (layoutNode.measurePending) { 574 sizeChanged = doRemeasure(layoutNode, constraints) 575 } 576 if (relayoutNeeded) { 577 if (layoutNode.layoutPending) { 578 val isPlacedByPlacedParent = 579 layoutNode === root || 580 (layoutNode.parent?.isPlaced == true && layoutNode.isPlacedByParent) 581 if (isPlacedByPlacedParent) { 582 if (layoutNode === root) { 583 layoutNode.place(0, 0) 584 } else { 585 layoutNode.replace() 586 } 587 onPositionedDispatcher.onNodePositioned(layoutNode) 588 // Since there has been an update to a coordinator somewhere in the 589 // modifier chain of this layout node, we might have onRectChanged 590 // callbacks that need to be notified of that change. As a result, even 591 // if the outer rect of this layout node hasn't changed, we want to 592 // invalidate the callbacks for them 593 layoutNode.requireOwner().rectManager.invalidateCallbacksFor(layoutNode) 594 consistencyChecker?.assertConsistent() 595 } 596 } 597 } 598 } 599 drainPostponedMeasureRequests() 600 } 601 return sizeChanged 602 } 603 604 private fun drainPostponedMeasureRequests() { 605 if (postponedMeasureRequests.isNotEmpty()) { 606 postponedMeasureRequests.forEach { request -> 607 if (request.node.isAttached) { 608 if (!request.isLookahead) { 609 request.node.requestRemeasure( 610 forceRequest = request.isForced, 611 invalidateIntrinsics = false 612 ) 613 } else { 614 request.node.requestLookaheadRemeasure( 615 forceRequest = request.isForced, 616 invalidateIntrinsics = false 617 ) 618 } 619 } 620 } 621 postponedMeasureRequests.clear() 622 } 623 } 624 625 /** 626 * Remeasures [layoutNode] if it has [LayoutNode.measurePending] or 627 * [LayoutNode.lookaheadMeasurePending]. 628 */ 629 private fun remeasureOnly(layoutNode: LayoutNode, affectsLookahead: Boolean) { 630 if (layoutNode.isDeactivated) { 631 return 632 } 633 val constraints = if (layoutNode === root) rootConstraints!! else null 634 if (affectsLookahead) { 635 doLookaheadRemeasure(layoutNode, constraints) 636 } else { 637 doRemeasure(layoutNode, constraints) 638 } 639 } 640 641 /** 642 * Makes sure the passed [layoutNode] and its subtree has the final sizes. The nodes which can 643 * potentially affect the parent size will be remeasured. 644 * 645 * The node or some of the nodes in its subtree can still be kept unmeasured if they are not 646 * placed and don't affect the parent size. See [requestRemeasure] for details. 647 */ 648 fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) { 649 // assert that it is executed during the `measureAndLayout` pass. 650 checkPrecondition(duringMeasureLayout) { 651 "forceMeasureTheSubtree should be executed during the measureAndLayout pass" 652 } 653 654 // if this node is not yet measured this invocation shouldn't be needed. 655 requirePrecondition(!layoutNode.measurePending(affectsLookahead)) { 656 "node not yet measured" 657 } 658 659 forceMeasureTheSubtreeInternal(layoutNode, affectsLookahead) 660 } 661 662 private fun onlyRemeasureIfPending(node: LayoutNode, affectsLookahead: Boolean) { 663 if (node.measurePending(affectsLookahead)) { 664 // we don't need to run relayout as part of this logic. so the node will 665 // not be removed from `relayoutNodes` in order to be visited again during 666 // the regular pass. it is important as the parent of this node can decide 667 // to not place this child, so the child relayout should be skipped. 668 remeasureAndRelayoutIfNeeded(node, affectsLookahead, relayoutNeeded = false) 669 } 670 } 671 672 private fun forceMeasureTheSubtreeInternal(layoutNode: LayoutNode, affectsLookahead: Boolean) { 673 layoutNode.forEachChild { child -> 674 // only proceed if child's size can affect the parent size 675 if ( 676 !affectsLookahead && child.measureAffectsParent || 677 affectsLookahead && child.measureAffectsParentLookahead 678 ) { 679 // When LookaheadRoot's parent gets forceMeasureSubtree call, we need to check 680 // both lookahead invalidation and non-lookahead invalidation, just like a measure() 681 // call from LookaheadRoot's parent would start the two tracks - lookahead and post 682 // lookahead measurements. 683 if (child.isOutMostLookaheadRoot && !affectsLookahead) { 684 // Force subtree measure hitting a lookahead root, pending lookahead measure. 685 // This could happen when the "applyChanges" cause nodes to be attached in 686 // lookahead subtree while the "applyChanges" is a part of the ancestor's 687 // subcomposition in the measure pass. 688 if (child.lookaheadMeasurePending && relayoutNodes.contains(child, true)) { 689 remeasureAndRelayoutIfNeeded(child, true, relayoutNeeded = false) 690 } else { 691 forceMeasureTheSubtree(child, true) 692 } 693 } 694 695 onlyRemeasureIfPending(child, affectsLookahead) 696 697 // if the child is still in NeedsRemeasure state then this child remeasure wasn't 698 // needed. it can happen for example when this child is not placed and can't affect 699 // the parent size. we can skip the whole subtree. 700 if (!child.measurePending(affectsLookahead)) { 701 // run recursively for the subtree. 702 forceMeasureTheSubtreeInternal(child, affectsLookahead) 703 } 704 } 705 } 706 707 // if the child was resized during the remeasurement it could request a remeasure on 708 // the parent. we need to remeasure now as this function assumes the whole subtree is 709 // fully measured as a result of the invocation. 710 onlyRemeasureIfPending(layoutNode, affectsLookahead) 711 } 712 713 /** 714 * Dispatch [OnPositionedModifier] callbacks for the nodes affected by the previous 715 * [measureAndLayout] execution. 716 * 717 * @param forceDispatch true means the whole tree should dispatch the callback (for example when 718 * the global position of the Owner has been changed) 719 */ 720 fun dispatchOnPositionedCallbacks(forceDispatch: Boolean = false) { 721 if (forceDispatch) { 722 onPositionedDispatcher.onRootNodePositioned(root) 723 } 724 onPositionedDispatcher.dispatch() 725 } 726 727 /** 728 * Removes [node] from the list of LayoutNodes being scheduled for the remeasure/relayout as it 729 * was detached. 730 */ 731 fun onNodeDetached(node: LayoutNode) { 732 relayoutNodes.remove(node) 733 onPositionedDispatcher.remove(node) 734 } 735 736 private val LayoutNode.measureAffectsParent 737 get() = 738 (measuredByParent == InMeasureBlock || 739 layoutDelegate.alignmentLinesOwner.alignmentLines.required) 740 741 /** Checks if there is a placed parent which size we can theoretically affect by remeasuring. */ 742 private val LayoutNode.measureAffectsPlacedParent: Boolean 743 get() { 744 var node = this 745 while ( 746 node.measureAffectsParent || 747 // if the parent is currently measuring, then measuredByParent on children was 748 // reset to NotUsed beforehand and does not represent the real usage. 749 node.parent?.layoutState == Measuring 750 ) { 751 val parent = node.parent ?: return false 752 if (parent.isPlaced) { 753 return true 754 } 755 node = parent 756 } 757 return false 758 } 759 760 private val LayoutNode.canAffectPlacedParent 761 get() = measurePending && measureAffectsPlacedParent 762 763 private val LayoutNode.canAffectParentInLookahead 764 get() = lookaheadMeasurePending && measureAffectsParentLookahead 765 766 private val LayoutNode.measureAffectsParentLookahead 767 get() = 768 (measuredByParentInLookahead == InMeasureBlock || 769 layoutDelegate.lookaheadAlignmentLinesOwner?.alignmentLines?.required == true) 770 771 private fun LayoutNode.measurePending(affectsLookahead: Boolean) = 772 if (affectsLookahead) lookaheadMeasurePending else measurePending 773 774 class PostponedRequest(val node: LayoutNode, val isLookahead: Boolean, val isForced: Boolean) 775 } 776