blob: 8a4ee37740bb2475b9a084aee546d3e69333b4d5 [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.
*/
@file:Suppress("NOTHING_TO_INLINE")
package androidx.compose.ui.node
import androidx.compose.ui.Measurable
import androidx.compose.ui.MeasureScope
import androidx.compose.ui.Placeable
import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusState2
import androidx.compose.ui.input.key.ModifiedKeyInputNode
import androidx.compose.ui.input.pointer.PointerInputFilter
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.platform.NativeRectF
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.minus
import androidx.compose.ui.unit.plus
import androidx.compose.ui.unit.toOffset
/**
* Measurable and Placeable type that has a position.
*/
@OptIn(ExperimentalLayoutNodeApi::class)
internal abstract class LayoutNodeWrapper(
internal val layoutNode: LayoutNode
) : Placeable(), Measurable, LayoutCoordinates {
internal open val wrapped: LayoutNodeWrapper? = null
internal var wrappedBy: LayoutNodeWrapper? = null
/**
* The scope used to measure the wrapped. InnerPlaceables are using the MeasureScope
* of the LayoutNode. For fewer allocations, everything else is reusing the measure scope of
* their wrapped.
*/
abstract val measureScope: MeasureScope
// Size exposed to LayoutCoordinates.
final override val size: IntSize get() = measuredSize
open val invalidateLayerOnBoundsChange = true
private var _measureResult: MeasureScope.MeasureResult? = null
var measureResult: MeasureScope.MeasureResult
get() = _measureResult ?: error(UnmeasuredError)
internal set(value) {
if (invalidateLayerOnBoundsChange &&
(value.width != _measureResult?.width || value.height != _measureResult?.height)
) {
findLayer()?.invalidate()
}
_measureResult = value
measuredSize = IntSize(measureResult.width, measureResult.height)
}
var position: IntOffset = IntOffset.Zero
internal set(value) {
if (invalidateLayerOnBoundsChange && value != field) {
findLayer()?.invalidate()
}
field = value
}
override val parentCoordinates: LayoutCoordinates?
get() {
check(isAttached) { ExpectAttachedLayoutCoordinates }
return layoutNode.outerLayoutNodeWrapper.wrappedBy
}
// True when the wrapper is running its own placing block to obtain the position of the
// wrapped, but is not interested in the position of the wrapped of the wrapped.
var isShallowPlacing = false
// TODO(mount): This is not thread safe.
private var rectCache: NativeRectF? = null
/**
* Whether a pointer that is relative to the device screen is in the bounds of this
* LayoutNodeWrapper.
*/
fun isGlobalPointerInBounds(globalPointerPosition: Offset): Boolean {
// TODO(shepshapard): Right now globalToLocal has to traverse the tree all the way back up
// so calling this is expensive. Would be nice to cache data such that this is cheap.
val localPointerPosition = globalToLocal(globalPointerPosition)
return localPointerPosition.x >= 0 &&
localPointerPosition.x < measuredSize.width &&
localPointerPosition.y >= 0 &&
localPointerPosition.y < measuredSize.height
}
/**
* Measures the modified child.
*/
abstract fun performMeasure(constraints: Constraints): Placeable
/**
* Measures the modified child.
*/
final override fun measure(constraints: Constraints): Placeable {
measurementConstraints = constraints
return performMeasure(constraints)
}
/**
* Places the modified child.
*/
abstract override fun place(position: IntOffset)
/**
* Draws the content of the LayoutNode
*/
abstract fun draw(canvas: Canvas)
/**
* Executes a hit test on any appropriate type associated with this [LayoutNodeWrapper].
*
* Override appropriately to either add a [PointerInputFilter] to [hitPointerInputFilters] or
* to pass the execution on.
*
* @param pointerPositionRelativeToScreen The tested pointer position, which is relative to
* the device screen.
* @param hitPointerInputFilters The collection that the hit [PointerInputFilter]s will be
* added to if hit.
*/
abstract fun hitTest(
pointerPositionRelativeToScreen: Offset,
hitPointerInputFilters: MutableList<PointerInputFilter>
)
override fun childToLocal(child: LayoutCoordinates, childLocal: Offset): Offset {
check(isAttached) { ExpectAttachedLayoutCoordinates }
check(child.isAttached) { "Child $child is not attached!" }
var wrapper = child as LayoutNodeWrapper
var position = childLocal
while (wrapper !== this) {
position = wrapper.toParentPosition(position)
val parent = wrapper.wrappedBy
check(parent != null) {
"childToLocal: child parameter is not a child of the LayoutCoordinates"
}
wrapper = parent
}
return position
}
override fun globalToLocal(global: Offset): Offset {
check(isAttached) { ExpectAttachedLayoutCoordinates }
val wrapper = wrappedBy ?: return fromParentPosition(
global - layoutNode.requireOwner().calculatePosition().toOffset()
)
return fromParentPosition(wrapper.globalToLocal(global))
}
override fun localToGlobal(local: Offset): Offset {
return localToRoot(local) + layoutNode.requireOwner().calculatePosition()
}
override fun localToRoot(local: Offset): Offset {
check(isAttached) { ExpectAttachedLayoutCoordinates }
var wrapper: LayoutNodeWrapper? = this
var position = local
while (wrapper != null) {
position = wrapper.toParentPosition(position)
wrapper = wrapper.wrappedBy
}
return position
}
protected inline fun withPositionTranslation(canvas: Canvas, block: (Canvas) -> Unit) {
val x = position.x.toFloat()
val y = position.y.toFloat()
canvas.translate(x, y)
block(canvas)
canvas.translate(-x, -y)
}
/**
* Converts [position] in the local coordinate system to a [Offset] in the
* [parentCoordinates] coordinate system.
*/
open fun toParentPosition(position: Offset): Offset = position + this.position
/**
* Converts [position] in the [parentCoordinates] coordinate system to a [Offset] in the
* local coordinate system.
*/
open fun fromParentPosition(position: Offset): Offset = position - this.position
protected fun drawBorder(canvas: Canvas, paint: Paint) {
val rect = Rect(
left = 0.5f,
top = 0.5f,
right = measuredSize.width.toFloat() - 0.5f,
bottom = measuredSize.height.toFloat() - 0.5f
)
canvas.drawRect(rect, paint)
}
/**
* Attaches the [LayoutNodeWrapper] and its wrapped [LayoutNodeWrapper] to an active
* LayoutNode.
*
* This will be called when the [LayoutNode] associated with this [LayoutNodeWrapper] is
* attached to the [Owner].
*
* It is also called whenever the modifier chain is replaced and the [LayoutNodeWrapper]s are
* recreated.
*/
abstract fun attach()
/**
* Detaches the [LayoutNodeWrapper] and its wrapped [LayoutNodeWrapper] from an active
* LayoutNode.
*
* This will be called when the [LayoutNode] associated with this [LayoutNodeWrapper] is
* detached from the [Owner].
*
* It is also called whenever the modifier chain is replaced and the [LayoutNodeWrapper]s are
* recreated.
*/
abstract fun detach()
/**
* Modifies bounds to be in the parent LayoutNodeWrapper's coordinates, including clipping,
* scaling, etc.
*/
protected open fun rectInParent(bounds: NativeRectF) {
val x = position.x
bounds.left += x
bounds.right += x
val y = position.y
bounds.top += y
bounds.bottom += y
}
override fun childBoundingBox(child: LayoutCoordinates): Rect {
check(isAttached) { ExpectAttachedLayoutCoordinates }
check(child.isAttached) { "Child $child is not attached!" }
val rectF = rectCache ?: NativeRectF().also { rectCache = it }
rectF.set(
0f,
0f,
child.size.width.toFloat(),
child.size.height.toFloat()
)
var wrapper = child as LayoutNodeWrapper
while (wrapper !== this) {
wrapper.rectInParent(rectF)
val parent = wrapper.wrappedBy
check(parent != null) {
"childToLocal: child parameter is not a child of the LayoutCoordinates"
}
wrapper = parent
}
return Rect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom
)
}
/**
* Returns the layer that this wrapper will draw into.
*/
open fun findLayer(): OwnedLayer? {
return if (layoutNode.innerLayerWrapper != null) {
wrappedBy?.findLayer()
} else {
layoutNode.findLayer()
}
}
/**
* Returns the first [ModifiedFocusNode] in the wrapper list that wraps this
* [LayoutNodeWrapper].
*
* TODO(b/160921940): Remove this function after removing ModifiedFocusNode.
*/
abstract fun findPreviousFocusWrapper(): ModifiedFocusNode?
/**
* Returns the first [focus node][ModifiedFocusNode2] in the wrapper list that wraps this
* [LayoutNodeWrapper].
*/
abstract fun findPreviousFocusWrapper2(): ModifiedFocusNode2?
/**
* Returns the next [ModifiedFocusNode] in the wrapper list that is wrapped by this
* [LayoutNodeWrapper].
*
* TODO(b/160921940): Remove this function after removing ModifiedFocusNode.
*/
abstract fun findNextFocusWrapper(): ModifiedFocusNode?
/**
* Returns the next [focus node][ModifiedFocusNode2] in the wrapper list that is wrapped by
* this [LayoutNodeWrapper].
*/
abstract fun findNextFocusWrapper2(): ModifiedFocusNode2?
/**
* Returns the last [ModifiedFocusNode] found following this [LayoutNodeWrapper]. It searches
* the wrapper list associated with this [LayoutNodeWrapper].
*
* TODO(b/160921940): Remove this function after removing ModifiedFocusNode.
*/
abstract fun findLastFocusWrapper(): ModifiedFocusNode?
/**
* Returns the last [focus node][ModifiedFocusNode2] found following this [LayoutNodeWrapper].
* It searches the wrapper list associated with this [LayoutNodeWrapper].
*/
abstract fun findLastFocusWrapper2(): ModifiedFocusNode2?
/**
* When the focus state changes, a [LayoutNodeWrapper] calls this function on the wrapper
* that wraps it. The focus state change must be propagated to the parents until we reach
* another focus node [ModifiedFocusNode2].
*/
@OptIn(ExperimentalFocus::class)
abstract fun propagateFocusStateChange(focusState: FocusState2)
/**
* Find the first ancestor that is a [ModifiedFocusNode].
*
* TODO(b/160921940): Remove this function after removing ModifiedFocusNode.
*/
internal fun findParentFocusNode(): ModifiedFocusNode? {
// TODO(b/152066829): We shouldn't need to search through the parentLayoutNode, as the
// wrappedBy property should automatically point to the last layoutWrapper of the parent.
// Find out why this doesn't work.
var focusParent = wrappedBy?.findPreviousFocusWrapper()
if (focusParent != null) {
return focusParent
}
var parentLayoutNode = layoutNode.parent
while (parentLayoutNode != null) {
focusParent = parentLayoutNode.outerLayoutNodeWrapper.findLastFocusWrapper()
if (focusParent != null) {
return focusParent
}
parentLayoutNode = parentLayoutNode.parent
}
return null
}
/**
* Find the first ancestor that is a [ModifiedFocusNode2].
*/
internal fun findParentFocusNode2(): ModifiedFocusNode2? {
// TODO(b/152066829): We shouldn't need to search through the parentLayoutNode, as the
// wrappedBy property should automatically point to the last layoutWrapper of the parent.
// Find out why this doesn't work.
var focusParent = wrappedBy?.findPreviousFocusWrapper2()
if (focusParent != null) {
return focusParent
}
var parentLayoutNode = layoutNode.parent
while (parentLayoutNode != null) {
focusParent = parentLayoutNode.outerLayoutNodeWrapper.findLastFocusWrapper2()
if (focusParent != null) {
return focusParent
}
parentLayoutNode = parentLayoutNode.parent
}
return null
}
/**
* Find the first ancestor that is a [ModifiedKeyInputNode].
*/
internal fun findParentKeyInputNode(): ModifiedKeyInputNode? {
// TODO(b/152066829): We shouldn't need to search through the parentLayoutNode, as the
// wrappedBy property should automatically point to the last layoutWrapper of the parent.
// Find out why this doesn't work.
var keyInputParent = wrappedBy?.findPreviousKeyInputWrapper()
if (keyInputParent != null) {
return keyInputParent
}
var parentLayoutNode = layoutNode.parent
while (parentLayoutNode != null) {
keyInputParent = parentLayoutNode.outerLayoutNodeWrapper.findLastKeyInputWrapper()
if (keyInputParent != null) {
return keyInputParent
}
parentLayoutNode = parentLayoutNode.parent
}
return null
}
/**
* Returns the first [ModifiedKeyInputNode] in the wrapper list that wraps this
* [LayoutNodeWrapper].
*/
abstract fun findPreviousKeyInputWrapper(): ModifiedKeyInputNode?
/**
* Returns the next [ModifiedKeyInputNode] in the wrapper list that is wrapped by this
* [LayoutNodeWrapper].
*/
abstract fun findNextKeyInputWrapper(): ModifiedKeyInputNode?
/**
* Returns the last [ModifiedFocusNode] found following this [LayoutNodeWrapper]. It searches
* the wrapper list associated with this [LayoutNodeWrapper]
*/
abstract fun findLastKeyInputWrapper(): ModifiedKeyInputNode?
/**
* Called when [LayoutNode.modifier] has changed and all the LayoutNodeWrappers have been
* configured.
*/
open fun onModifierChanged() {}
internal companion object {
const val ExpectAttachedLayoutCoordinates = "LayoutCoordinate operations are only valid " +
"when isAttached is true"
const val UnmeasuredError = "Asking for measurement result of unmeasured layout modifier"
}
}