[go: nahoru, domu]

blob: f7a0feccff14bd4850c55cb9c258f56251d1eab0 [file] [log] [blame]
/*
* 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.compose.ui.node
import androidx.compose.ui.node.LayoutNode.LayoutState.LayingOut
import androidx.compose.ui.node.LayoutNode.LayoutState.Measuring
import androidx.compose.ui.node.LayoutNode.LayoutState.NeedsRelayout
import androidx.compose.ui.node.LayoutNode.LayoutState.NeedsRemeasure
import androidx.compose.ui.node.LayoutNode.LayoutState.Ready
import androidx.compose.ui.node.LayoutNode.UsageByParent.InLayoutBlock
import androidx.compose.ui.node.LayoutNode.UsageByParent.InMeasureBlock
import androidx.compose.ui.node.LayoutNode.UsageByParent.NotUsed
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.util.fastForEach
/**
* Keeps track of [LayoutNode]s which needs to be remeasured or relaid out.
*
* Use [requestRemeasure] to schedule remeasuring or [requestRelayout] to schedule relayout.
*
* Use [measureAndLayout] to perform scheduled actions and [dispatchOnPositionedCallbacks] to
* dispatch [OnPositionedModifier] callbacks for the nodes affected by the previous
* [measureAndLayout] execution.
*/
internal class MeasureAndLayoutDelegate(private val root: LayoutNode) {
/**
* LayoutNodes that need measure or layout.
*/
private val relayoutNodes = DepthSortedSet(Owner.enableExtraAssertions)
/**
* Whether any LayoutNode needs measure or layout.
*/
val hasPendingMeasureOrLayout get() = relayoutNodes.isNotEmpty()
/**
* Flag to indicate that we're currently measuring.
*/
private var duringMeasureLayout = false
/**
* Dispatches on positioned callbacks.
*/
private val onPositionedDispatcher = OnPositionedDispatcher()
/**
* The current measure iteration. The value is incremented during the [measureAndLayout]
* execution. Some [measureAndLayout] executions will increment it more than once.
*/
var measureIteration: Long = 1L
get() {
require(duringMeasureLayout) {
"measureIteration should be only used during the measure/layout pass"
}
return field
}
private set
/**
* Stores the list of [LayoutNode]s scheduled to be remeasured in the next measure/layout pass.
* We were unable to mark them as needsRemeasure=true previously as this request happened
* during the previous measure/layout pass and they were already measured as part of it.
* See [requestRemeasure] for more details.
*/
private val postponedMeasureRequests = mutableListOf<LayoutNode>()
private var rootConstraints: Constraints? = null
/**
* @param constraints The constraints to measure the root [LayoutNode] with
*/
fun updateRootConstraints(constraints: Constraints) {
if (rootConstraints != constraints) {
require(!duringMeasureLayout)
rootConstraints = constraints
root.layoutState = NeedsRemeasure
relayoutNodes.add(root)
}
}
private val consistencyChecker: LayoutTreeConsistencyChecker? =
if (Owner.enableExtraAssertions) {
LayoutTreeConsistencyChecker(
root,
relayoutNodes,
postponedMeasureRequests
)
} else {
null
}
/**
* Requests remeasure for this [layoutNode] and nodes affected by its measure result.
*
* @return returns true if the [measureAndLayout] execution should be scheduled as a result
* of the request.
*/
fun requestRemeasure(layoutNode: LayoutNode): Boolean = when (layoutNode.layoutState) {
Measuring, NeedsRemeasure -> {
// requestMeasure has already been called for this node or
// we're currently measuring it, let's swallow. example when it happens: we compose
// DataNode inside BoxWithConstraints, this calls onRequestMeasure on DataNode's
// parent, but this parent is BoxWithConstraints which is currently measuring.
false
}
LayingOut -> {
// requestMeasure is currently laying out and it is incorrect to request remeasure
// now, let's postpone it.
postponedMeasureRequests.add(layoutNode)
consistencyChecker?.assertConsistent()
false
}
NeedsRelayout, Ready -> {
if (duringMeasureLayout && layoutNode.wasMeasuredDuringThisIteration) {
postponedMeasureRequests.add(layoutNode)
} else {
layoutNode.layoutState = NeedsRemeasure
if (layoutNode.isPlaced || layoutNode.canAffectParent) {
val parentLayoutState = layoutNode.parent?.layoutState
if (parentLayoutState != NeedsRemeasure) {
relayoutNodes.add(layoutNode)
}
}
}
!duringMeasureLayout
}
}
/**
* Requests relayout for this [layoutNode] and nodes affected by its position.
*
* @return returns true if the [measureAndLayout] execution should be scheduled as a result
* of the request.
*/
fun requestRelayout(layoutNode: LayoutNode): Boolean = when (layoutNode.layoutState) {
Measuring, NeedsRemeasure, NeedsRelayout, LayingOut -> {
// don't need to do anything else since the parent is already scheduled
// for a relayout (measure will trigger relayout), or is laying out right now
consistencyChecker?.assertConsistent()
false
}
Ready -> {
layoutNode.layoutState = NeedsRelayout
if (layoutNode.isPlaced) {
val parentLayoutState = layoutNode.parent?.layoutState
if (parentLayoutState != NeedsRemeasure && parentLayoutState != NeedsRelayout) {
relayoutNodes.add(layoutNode)
}
}
!duringMeasureLayout
}
}
private fun doRemeasure(layoutNode: LayoutNode, rootConstraints: Constraints): Boolean {
val sizeChanged = if (layoutNode === root) {
layoutNode.remeasure(rootConstraints)
} else {
layoutNode.remeasure()
}
val parent = layoutNode.parent
if (sizeChanged) {
if (parent == null) {
return true
} else if (layoutNode.measuredByParent == InMeasureBlock) {
requestRemeasure(parent)
} else {
require(layoutNode.measuredByParent == InLayoutBlock)
requestRelayout(parent)
}
}
return false
}
/**
* Iterates through all LayoutNodes that have requested layout and measures and lays them out
*/
fun measureAndLayout(): Boolean {
require(root.isAttached)
require(root.isPlaced)
require(!duringMeasureLayout)
// we don't need to measure any children unless we have the correct root constraints
val rootConstraints = rootConstraints ?: return false
var rootNodeResized = false
if (relayoutNodes.isNotEmpty()) {
duringMeasureLayout = true
relayoutNodes.popEach { layoutNode ->
val alignmentLinesOwner = layoutNode.alignmentLinesQueryOwner
if (layoutNode.isPlaced ||
layoutNode.canAffectParent ||
(
alignmentLinesOwner != null && alignmentLinesOwner
.alignmentUsageByParent != NotUsed
)
) {
if (layoutNode.layoutState == NeedsRemeasure) {
if (doRemeasure(layoutNode, rootConstraints)) {
rootNodeResized = true
}
}
if (layoutNode.layoutState == NeedsRelayout && layoutNode.isPlaced) {
if (layoutNode === root) {
layoutNode.place(0, 0)
} else {
layoutNode.replace()
}
onPositionedDispatcher.onNodePositioned(layoutNode)
consistencyChecker?.assertConsistent()
}
measureIteration++
// execute postponed `onRequestMeasure`
if (postponedMeasureRequests.isNotEmpty()) {
postponedMeasureRequests.fastForEach {
if (it.isAttached) {
requestRemeasure(it)
}
}
postponedMeasureRequests.clear()
}
}
}
duringMeasureLayout = false
consistencyChecker?.assertConsistent()
}
return rootNodeResized
}
/**
* Dispatch [OnPositionedModifier] callbacks for the nodes affected by the previous
* [measureAndLayout] execution.
*
* @param forceDispatch true means the whole tree should dispatch the callback (for example
* when the global position of the Owner has been changed)
*/
fun dispatchOnPositionedCallbacks(forceDispatch: Boolean = false) {
if (forceDispatch) {
onPositionedDispatcher.onRootNodePositioned(root)
}
onPositionedDispatcher.dispatch()
}
/**
* Removes [node] from the list of LayoutNodes being scheduled for the remeasure/relayout as
* it was detached.
*/
fun onNodeDetached(node: LayoutNode) {
relayoutNodes.remove(node)
}
private val LayoutNode.canAffectParent
get() = layoutState == NeedsRemeasure &&
(measuredByParent == InMeasureBlock || alignmentLinesQueryOwner != null)
}