blob: 843b9247e49ab9dbb7073f78ce75599feadce731 [file] [log] [blame]
/*
* Copyright 2022 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.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadLayoutCoordinates
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.PlacementScope
import androidx.compose.ui.layout.VerticalAlignmentLine
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
/**
* This is the base class for NodeCoordinator and LookaheadDelegate. The common
* functionalities between the two are extracted here.
*/
internal abstract class LookaheadCapablePlaceable : Placeable(), MeasureScopeWithLayoutNode {
abstract val position: IntOffset
abstract val child: LookaheadCapablePlaceable?
abstract val parent: LookaheadCapablePlaceable?
abstract val hasMeasureResult: Boolean
abstract override val layoutNode: LayoutNode
abstract val coordinates: LayoutCoordinates
final override fun get(alignmentLine: AlignmentLine): Int {
if (!hasMeasureResult) return AlignmentLine.Unspecified
val measuredPosition = calculateAlignmentLine(alignmentLine)
if (measuredPosition == AlignmentLine.Unspecified) return AlignmentLine.Unspecified
return measuredPosition + if (alignmentLine is VerticalAlignmentLine) {
apparentToRealOffset.x
} else {
apparentToRealOffset.y
}
}
abstract fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int
// True when the coordinator is running its own placing block to obtain the position
// in parent, but is not interested in the position of children.
internal var isShallowPlacing: Boolean = false
internal abstract val measureResult: MeasureResult
internal abstract fun replace()
abstract val alignmentLinesOwner: AlignmentLinesOwner
/**
* Used to indicate that this placement pass is for the purposes of calculating an
* alignment line. If it is, then
* [LayoutNodeLayoutDelegate.coordinatesAccessedDuringPlacement] will be changed
* when [Placeable.PlacementScope.coordinates] is accessed to indicate that the placement
* is not finalized and must be run again.
*/
internal var isPlacingForAlignment = false
/**
* [PlacementScope] used to place children.
*/
val placementScope = PlacementScope(this)
protected fun NodeCoordinator.invalidateAlignmentLinesFromPositionChange() {
if (wrapped?.layoutNode != layoutNode) {
alignmentLinesOwner.alignmentLines.onAlignmentsChanged()
} else {
alignmentLinesOwner.parentAlignmentLinesOwner?.alignmentLines?.onAlignmentsChanged()
}
}
@OptIn(ExperimentalComposeUiApi::class)
override val isLookingAhead: Boolean
get() = false
override fun layout(
width: Int,
height: Int,
alignmentLines: Map<AlignmentLine, Int>,
placementBlock: PlacementScope.() -> Unit
): MeasureResult {
checkMeasuredSize(width, height)
return object : MeasureResult {
override val width: Int
get() = width
override val height: Int
get() = height
override val alignmentLines: Map<AlignmentLine, Int>
get() = alignmentLines
override fun placeChildren() {
placementScope.placementBlock()
}
}
}
}
// This is about 16 million pixels. That should be big enough. We'll treat anything bigger as an
// error.
private const val MaxLayoutDimension = (1 shl 24) - 1
private const val MaxLayoutMask: Int = 0xFF00_0000.toInt()
@Suppress("NOTHING_TO_INLINE")
internal inline fun checkMeasuredSize(width: Int, height: Int) {
check(width and MaxLayoutMask == 0 && height and MaxLayoutMask == 0) {
"Size($width x $height) is out of range. Each dimension must be between 0 and " +
"$MaxLayoutDimension."
}
}
internal abstract class LookaheadDelegate(
val coordinator: NodeCoordinator,
) : Measurable, LookaheadCapablePlaceable() {
override val child: LookaheadCapablePlaceable?
get() = coordinator.wrapped?.lookaheadDelegate
override val hasMeasureResult: Boolean
get() = _measureResult != null
override var position = IntOffset.Zero
private var oldAlignmentLines: MutableMap<AlignmentLine, Int>? = null
override val measureResult: MeasureResult
get() = _measureResult ?: error(
"LookaheadDelegate has not been measured yet when measureResult is requested."
)
override val isLookingAhead: Boolean
get() = true
override val layoutDirection: LayoutDirection
get() = coordinator.layoutDirection
override val density: Float
get() = coordinator.density
override val fontScale: Float
get() = coordinator.fontScale
override val parent: LookaheadCapablePlaceable?
get() = coordinator.wrappedBy?.lookaheadDelegate
override val layoutNode: LayoutNode
get() = coordinator.layoutNode
override val coordinates: LayoutCoordinates
get() = lookaheadLayoutCoordinates
val lookaheadLayoutCoordinates = LookaheadLayoutCoordinates(this)
override val alignmentLinesOwner: AlignmentLinesOwner
get() = coordinator.layoutNode.layoutDelegate.lookaheadAlignmentLinesOwner!!
private var _measureResult: MeasureResult? = null
set(result) {
result?.let {
measuredSize = IntSize(it.width, it.height)
} ?: run { measuredSize = IntSize.Zero }
if (field != result && result != null) {
// We do not simply compare against old.alignmentLines in case this is a
// MutableStateMap and the same instance might be passed.
if ((!oldAlignmentLines.isNullOrEmpty() || result.alignmentLines.isNotEmpty()) &&
result.alignmentLines != oldAlignmentLines
) {
alignmentLinesOwner.alignmentLines.onAlignmentsChanged()
@Suppress("PrimitiveInCollection")
val oldLines = oldAlignmentLines
?: (mutableMapOf<AlignmentLine, Int>().also { oldAlignmentLines = it })
oldLines.clear()
oldLines.putAll(result.alignmentLines)
}
}
field = result
}
protected val cachedAlignmentLinesMap = mutableMapOf<AlignmentLine, Int>()
internal fun getCachedAlignmentLine(alignmentLine: AlignmentLine): Int =
cachedAlignmentLinesMap[alignmentLine] ?: AlignmentLine.Unspecified
override fun replace() {
placeAt(position, 0f, null)
}
final override fun placeAt(
position: IntOffset,
zIndex: Float,
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
placeSelf(position)
if (isShallowPlacing) return
placeChildren()
}
private fun placeSelf(position: IntOffset) {
if (this.position != position) {
this.position = position
layoutNode.layoutDelegate.lookaheadPassDelegate
?.notifyChildrenUsingCoordinatesWhilePlacing()
coordinator.invalidateAlignmentLinesFromPositionChange()
}
}
internal fun placeSelfApparentToRealOffset(position: IntOffset) {
placeSelf(position + apparentToRealOffset)
}
protected open fun placeChildren() {
measureResult.placeChildren()
}
inline fun performingMeasure(
constraints: Constraints,
block: () -> MeasureResult
): Placeable {
measurementConstraints = constraints
_measureResult = block()
return this
}
override val parentData: Any?
get() = coordinator.parentData
override fun minIntrinsicWidth(height: Int): Int {
return coordinator.wrapped!!.lookaheadDelegate!!.minIntrinsicWidth(height)
}
override fun maxIntrinsicWidth(height: Int): Int {
return coordinator.wrapped!!.lookaheadDelegate!!.maxIntrinsicWidth(height)
}
override fun minIntrinsicHeight(width: Int): Int {
return coordinator.wrapped!!.lookaheadDelegate!!.minIntrinsicHeight(width)
}
override fun maxIntrinsicHeight(width: Int): Int {
return coordinator.wrapped!!.lookaheadDelegate!!.maxIntrinsicHeight(width)
}
internal fun positionIn(ancestor: LookaheadDelegate): IntOffset {
var aggregatedOffset = IntOffset.Zero
var lookaheadDelegate = this
while (lookaheadDelegate != ancestor) {
aggregatedOffset += lookaheadDelegate.position
lookaheadDelegate = lookaheadDelegate.coordinator.wrappedBy!!.lookaheadDelegate!!
}
return aggregatedOffset
}
}