| /* |
| * Copyright 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.ui.core |
| |
| import androidx.ui.core.LayoutNode.LayoutState |
| import androidx.ui.util.fastForEach |
| |
| /** |
| * There are some contracts between the tree of LayoutNodes and the state of AndroidComposeView |
| * which is hard to enforce but important to maintain. This method is intended to do the |
| * work only during our tests and will iterate through the tree to validate the states consistency. |
| */ |
| @OptIn(ExperimentalLayoutNodeApi::class) |
| internal class LayoutTreeConsistencyChecker( |
| private val root: LayoutNode, |
| private val relayoutNodes: DepthSortedSet, |
| private val postponedMeasureRequests: List<LayoutNode> |
| ) { |
| fun assertConsistent() { |
| val inconsistencyFound = !isTreeConsistent(root) |
| if (inconsistencyFound) { |
| throw IllegalStateException("Inconsistency found! ${logTree()}") |
| } |
| } |
| |
| private fun isTreeConsistent(node: LayoutNode): Boolean { |
| if (!node.consistentLayoutState()) { |
| return false |
| } |
| node.children.fastForEach { |
| if (!isTreeConsistent(it)) { |
| return@isTreeConsistent false |
| } |
| } |
| return true |
| } |
| |
| private fun LayoutNode.consistentLayoutState(): Boolean { |
| if (isPlaced) { |
| if (layoutState == LayoutState.NeedsRemeasure && |
| postponedMeasureRequests.contains(this) |
| ) { |
| // this node is waiting to be measured by parent or if this will not happen |
| // `onRequestMeasure` will be called for all items in `postponedMeasureRequests` |
| return true |
| } |
| // remeasure or relayout is scheduled |
| val parentLayoutState = parent?.layoutState |
| if (layoutState == LayoutState.NeedsRemeasure) { |
| return relayoutNodes.contains(this) || |
| parentLayoutState == LayoutState.NeedsRemeasure |
| } |
| if (layoutState == LayoutState.NeedsRelayout) { |
| return relayoutNodes.contains(this) || |
| parentLayoutState == LayoutState.NeedsRemeasure || |
| parentLayoutState == LayoutState.NeedsRelayout |
| } |
| } |
| return true |
| } |
| |
| private fun nodeToString(node: LayoutNode): String { |
| return with(StringBuilder()) { |
| append(node) |
| append("[${node.layoutState}]") |
| if (!node.isPlaced) append("[!isPlaced]") |
| append("[measuredByParent=${node.measuredByParent}]") |
| if (!node.consistentLayoutState()) { |
| append("[INCONSISTENT]") |
| } |
| toString() |
| } |
| } |
| |
| /** Prints the nodes tree into the logs. */ |
| private fun logTree(): String { |
| val stringBuilder = StringBuilder() |
| fun printSubTree(node: LayoutNode, depth: Int) { |
| var childrenDepth = depth |
| val nodeRepresentation = nodeToString(node) |
| if (nodeRepresentation.isNotEmpty()) { |
| for (i in 0 until depth) { |
| stringBuilder.append("..") |
| } |
| stringBuilder.appendLine(nodeRepresentation) |
| childrenDepth += 1 |
| } |
| node.children.fastForEach { printSubTree(it, childrenDepth) } |
| } |
| stringBuilder.appendLine("Tree state:") |
| printSubTree(root, 0) |
| return stringBuilder.toString() |
| } |
| } |