1 /* 2 * 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.ui.util.fastAny 20 import androidx.compose.ui.util.fastFirstOrNull 21 import androidx.compose.ui.util.fastForEach 22 23 /** 24 * There are some contracts between the tree of LayoutNodes and the state of AndroidComposeView 25 * which is hard to enforce but important to maintain. This method is intended to do the work only 26 * during our tests and will iterate through the tree to validate the states consistency. 27 */ 28 internal class LayoutTreeConsistencyChecker( 29 private val root: LayoutNode, 30 private val relayoutNodes: DepthSortedSetsForDifferentPasses, 31 private val postponedMeasureRequests: List<MeasureAndLayoutDelegate.PostponedRequest> 32 ) { assertConsistentnull33 fun assertConsistent() { 34 val inconsistencyFound = !isTreeConsistent(root) 35 if (inconsistencyFound) { 36 println(logTree()) 37 throw IllegalStateException("Inconsistency found!") 38 } 39 } 40 isTreeConsistentnull41 private fun isTreeConsistent(node: LayoutNode): Boolean { 42 if (!node.consistentLayoutState()) { 43 return false 44 } 45 node.children.fastForEach { 46 if (!isTreeConsistent(it)) { 47 return@isTreeConsistent false 48 } 49 } 50 return true 51 } 52 LayoutNodenull53 private fun LayoutNode.consistentLayoutState(): Boolean { 54 val parent = this.parent 55 val parentLayoutState = parent?.layoutState 56 if (isPlaced || placeOrder != LayoutNode.NotPlacedPlaceOrder && parent?.isPlaced == true) { 57 if ( 58 measurePending && 59 postponedMeasureRequests.fastFirstOrNull { 60 it.node == this && !it.isLookahead 61 } != null 62 ) { 63 // this node is waiting to be measured by parent or if this will not happen 64 // `onRequestMeasure` will be called for all items in `postponedMeasureRequests` 65 return true 66 } 67 if (isDeactivated) { 68 // remeasure/relayout requests for deactivated nodes are ignored 69 return true 70 } 71 // remeasure or relayout is scheduled 72 if (measurePending) { 73 return relayoutNodes.contains(this) || 74 layoutState == LayoutNode.LayoutState.LookaheadMeasuring || 75 parent?.measurePending == true || 76 parent?.lookaheadMeasurePending == true || 77 parentLayoutState == LayoutNode.LayoutState.Measuring 78 } 79 if (layoutPending) { 80 return relayoutNodes.contains(this) || 81 parent == null || 82 parent.measurePending || 83 parent.layoutPending || 84 parentLayoutState == LayoutNode.LayoutState.Measuring || 85 parentLayoutState == LayoutNode.LayoutState.LayingOut || 86 postponedMeasureRequests.fastAny { it.node == this } || 87 layoutState == LayoutNode.LayoutState.Measuring 88 } 89 } 90 if (isPlacedInLookahead == true) { 91 if ( 92 lookaheadMeasurePending && 93 postponedMeasureRequests.fastFirstOrNull { 94 it.node == this && it.isLookahead 95 } != null 96 ) { 97 // this node is waiting to be lookahead measured by parent or if this will not 98 // happen `onRequestLookaheadMeasure` will be called for all items in 99 // `postponedLookaheadMeasureRequests` 100 return true 101 } 102 if (lookaheadMeasurePending) { 103 return relayoutNodes.contains(this, true) || 104 parent?.lookaheadMeasurePending == true || 105 parentLayoutState == LayoutNode.LayoutState.LookaheadMeasuring || 106 (parent?.measurePending == true && lookaheadRoot == this) 107 } 108 if (lookaheadLayoutPending) { 109 return relayoutNodes.contains(this, true) || 110 parent == null || 111 parent.lookaheadMeasurePending || 112 parent.lookaheadLayoutPending || 113 parentLayoutState == LayoutNode.LayoutState.LookaheadMeasuring || 114 parentLayoutState == LayoutNode.LayoutState.LookaheadLayingOut || 115 (parent.layoutPending && lookaheadRoot == this) 116 } 117 } 118 return true 119 } 120 nodeToStringnull121 private fun nodeToString(node: LayoutNode): String { 122 return with(StringBuilder()) { 123 append(node) 124 append("[${node.layoutState}]") 125 if (!node.isPlaced) append("[!isPlaced]") 126 append("[measuredByParent=${node.measuredByParent}]") 127 if (!node.consistentLayoutState()) { 128 append("[INCONSISTENT]") 129 } 130 toString() 131 } 132 } 133 134 /** Prints the nodes tree into the logs. */ logTreenull135 private fun logTree(): String { 136 val stringBuilder = StringBuilder() 137 fun printSubTree(node: LayoutNode, depth: Int) { 138 var childrenDepth = depth 139 val nodeRepresentation = nodeToString(node) 140 if (nodeRepresentation.isNotEmpty()) { 141 for (i in 0 until depth) { 142 stringBuilder.append("..") 143 } 144 stringBuilder.appendLine(nodeRepresentation) 145 childrenDepth += 1 146 } 147 node.children.fastForEach { printSubTree(it, childrenDepth) } 148 } 149 stringBuilder.appendLine("Tree state:") 150 printSubTree(root, 0) 151 return stringBuilder.toString() 152 } 153 } 154