[go: nahoru, domu]

blob: af36a5d4b6be325301cc7daeba0067b9a676825c [file] [log] [blame]
/*
* Copyright 2019 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.focus.FocusModifierImpl
import androidx.ui.core.focus.ModifiedFocusNode
import androidx.ui.core.focus.findParentFocusNode
import androidx.ui.core.focus.ownerHasFocus
import androidx.ui.core.focus.requestFocusForOwner
import androidx.ui.core.pointerinput.PointerInputFilter
import androidx.ui.core.pointerinput.PointerInputModifier
import androidx.ui.core.semantics.SemanticsModifier
import androidx.ui.core.semantics.SemanticsWrapper
import androidx.ui.focus.FocusDetailedState
import androidx.ui.focus.FocusDetailedState.Active
import androidx.ui.focus.FocusDetailedState.ActiveParent
import androidx.ui.focus.FocusDetailedState.Captured
import androidx.ui.focus.FocusDetailedState.Disabled
import androidx.ui.focus.FocusDetailedState.Inactive
import androidx.ui.focus.FocusModifier
import androidx.ui.graphics.Canvas
import androidx.ui.unit.Density
import androidx.ui.unit.IntPx
import androidx.ui.unit.PxPosition
import androidx.ui.unit.PxSize
import androidx.ui.unit.round
import androidx.ui.util.fastForEach
/**
* Enable to log changes to the ComponentNode tree. This logging is quite chatty.
*/
private const val DebugChanges = false
/**
* The base type for all nodes from the tree generated from a component hierarchy.
*
* Specific components are backed by a tree of nodes: Draw, Layout, GestureDetector.
* All other components are not represented in the backing hierarchy.
*/
sealed class ComponentNode {
internal val children = mutableListOf<ComponentNode>()
/**
* The parent node in the ComponentNode hierarchy. This is `null` when the `ComponentNode`
* is attached (has an [owner]) and is the root of the tree or has not had [add] called for it.
*/
var parent: ComponentNode? = null
private set
/**
* The view system [Owner]. This `null` until [attach] is called
*/
var owner: Owner? = null
private set
/**
* The tree depth of the ComponentNode. This is valid only when [isAttached] is true.
*/
var depth: Int = 0
/**
* An opaque value set by the [Owner]. It is `null` when [isAttached] is false, but
* may also be `null` when [isAttached] is true, depending on the needs of the Owner.
*/
var ownerData: Any? = null
/**
* Returns the number of children in this ComponentNode.
*/
val count: Int
get() = children.size
/**
* This is the LayoutNode ancestor that contains this LayoutNode. This will be `null` for the
* root [LayoutNode].
*/
open val parentLayoutNode: LayoutNode?
get() = containingLayoutNode
/**
* Method to find the layout node. LayoutNode returns itself, but
* all other ComponentNodes return the parent's `containingLayoutNode`.
*/
internal open val containingLayoutNode: LayoutNode?
get() = parent?.containingLayoutNode
/**
* Execute [block] on all children of this ComponentNode.
*/
inline fun visitChildren(block: (ComponentNode) -> Unit) {
for (i in 0 until count) {
block(this[i])
}
}
/**
* Execute [block] on all children of this ComponentNode in reverse order.
*/
inline fun visitChildrenReverse(block: (ComponentNode) -> Unit) {
for (i in count - 1 downTo 0) {
block(this[i])
}
}
/**
* Inserts a child [ComponentNode] at a particular index. If this ComponentNode [isAttached]
* then [instance] will become [attach]ed also. [instance] must have a `null` [parent].
*/
fun insertAt(index: Int, instance: ComponentNode) {
ErrorMessages.ComponentNodeHasParent.validateState(instance.parent == null)
ErrorMessages.OwnerAlreadyAttached.validateState(instance.owner == null)
if (DebugChanges) {
println("$instance added to $this at index $index")
}
instance.parent = this
children.add(index, instance)
val containingLayout = containingLayoutNode
if (containingLayout != null) {
if (instance is LayoutNode) {
instance.layoutNodeWrapper.wrappedBy = containingLayout.innerLayoutNodeWrapper
} else {
visitLayoutChildren {
it.layoutNodeWrapper.wrappedBy = containingLayout.innerLayoutNodeWrapper
}
}
}
val owner = this.owner
if (owner != null) {
instance.attach(owner)
}
}
/**
* Removes one or more children, starting at [index].
*/
fun removeAt(index: Int, count: Int) {
ErrorMessages.CountOutOfRange.validateArg(count >= 0, count)
val attached = owner != null
for (i in index + count - 1 downTo index) {
val child = children.removeAt(i)
if (DebugChanges) {
println("$child removed from $this at index $i")
}
if (attached) {
child.detach()
}
child.parent = null
}
}
/**
* Moves [count] elements starting at index [from] to index [to]. The [to] index is related to
* the position before the change, so, for example, to move an element at position 1 to after
* the element at position 2, [from] should be `1` and [to] should be `3`. If the elements
* were ComponentNodes A B C D E, calling `move(1, 3, 1)` would result in the ComponentNodes
* being reordered to A C B D E.
*/
fun move(from: Int, to: Int, count: Int) {
if (from == to) {
return // nothing to do
}
for (i in 0 until count) {
// if "from" is after "to," the from index moves because we're inserting before it
val fromIndex = if (from > to) from + i else from
val toIndex = if (from > to) to + i else to + count - 2
val child = children.removeAt(fromIndex)
if (DebugChanges) {
println("$child moved in $this from index $fromIndex to $toIndex")
}
children.add(toIndex, child)
}
containingLayoutNode?.let {
it.layoutChildrenDirty = true
it.requestRemeasure()
}
}
/**
* Returns the child ComponentNode at the given index. An exception will be thrown if there
* is no child at the given index.
*/
operator fun get(index: Int): ComponentNode = children[index]
/**
* Set the [Owner] of this ComponentNode. This ComponentNode must not already be attached.
* [owner] must match its [parent].[owner].
*/
open fun attach(owner: Owner) {
ErrorMessages.OwnerAlreadyAttached.validateState(this.owner == null)
val parent = parent
ErrorMessages.ParentOwnerMustMatchChild.validateState(
parent == null ||
parent.owner == owner
)
this.owner = owner
this.depth = (parent?.depth ?: -1) + 1
owner.onAttach(this)
visitChildren { child ->
child.attach(owner)
}
}
/**
* Remove the ComponentNode from the [Owner]. The [owner] must not be `null` before this call
* and its [parent]'s [owner] must be `null` before calling this. This will also [detach] all
* children. After executing, the [owner] will be `null`.
*/
open fun detach() {
val owner = owner ?: ErrorMessages.OwnerAlreadyDetached.state()
owner.onDetach(this)
this.owner = null
depth = 0
visitChildren { child ->
child.detach()
}
}
private val _zIndexSortedChildren = mutableListOf<ComponentNode>()
/**
* Returns the children list sorted by their [LayoutNode.zIndex].
* Note that the object is reused so you shouldn't save it for later.
*/
@PublishedApi
internal val zIndexSortedChildren: List<ComponentNode>
get() {
_zIndexSortedChildren.clear()
_zIndexSortedChildren.addAll(children)
_zIndexSortedChildren.sortWith(ZIndexComparator)
return _zIndexSortedChildren
}
override fun toString(): String {
return "${simpleIdentityToString(this)} children: ${children.size}"
}
/**
* Call this method from the debugger to see a dump of the ComponentNode tree structure
*/
private fun debugTreeToString(depth: Int = 0): String {
val tree = StringBuilder()
for (i in 0 until depth) {
tree.append(" ")
}
tree.append("|-")
tree.append(toString())
tree.append('\n')
for (child in children) {
tree.append(child.debugTreeToString(depth + 1))
}
if (depth == 0) {
// Delete trailing newline
tree.deleteCharAt(tree.length - 1)
}
return tree.toString()
}
}
/**
* Comparator allowing to sort nodes by zIndex
*/
private val ZIndexComparator = Comparator<ComponentNode> { node1, node2 ->
val depth1 = if (node1 is LayoutNode) node1.zIndex else 0f
val depth2 = if (node2 is LayoutNode) node2.zIndex else 0f
if (depth1 > depth2) 1 else if (depth1 < depth2) -1 else 0
}
/**
* Returns true if this [ComponentNode] currently has an [ComponentNode.owner]. Semantically,
* this means that the ComponentNode is currently a part of a component tree.
*/
fun ComponentNode.isAttached() = owner != null
// TODO(b/143778512): Why are the properties vars? Shouldn't they be vals defined in the
// constructor such that they both must be provided?
/**
* Backing node for handling pointer events.
*/
class PointerInputNode : ComponentNode() {
/**
* Invoked right after the [PointerInputNode] is hit by a pointer during hit testing.
*
* @See CustomEventDispatcher
*/
var initHandler: ((CustomEventDispatcher) -> Unit)? = null
/**
* Invoked when pointers that previously hit this PointerInputNode have changed.
*/
var pointerInputHandler: PointerInputHandler = { event, _, _ -> event }
/**
* Invoked when a [CustomEvent] is dispatched by a [PointerInputNode].
*
* Dispatch occurs over all passes of [PointerEventPass].
*
* The [CustomEvent] is the event being dispatched. The [PointerEventPass] is the pass that
* dispatch is currently on.
*
* @see CustomEvent
* @see PointerEventPass
*/
var customEventHandler: ((CustomEvent, PointerEventPass) -> Unit)? = null
/**
* Invoked to notify the handler that no more calls to pointerInputHandler will be made, until
* at least new pointers exist. This can occur for a few reasons:
* 1. Android dispatches ACTION_CANCEL to [AndroidComposeView.onTouchEvent].
* 2. The PointerInputNode has been removed from the compose hierarchy.
* 3. The PointerInputNode no longer has any descendant [LayoutNode]s and therefore does not
* know what region of the screen it should virtually exist in.
*/
var cancelHandler: () -> Unit = { }
}
/**
* Backing node that implements focus.
*/
class FocusNode : ComponentNode() {
/**
* Implementation oddity around composition; used to capture a reference to this
* [FocusNode] when composed. This is a reverse property that mutates its right-hand side.
*
* TODO: Once we finalize the API consider removing this and replace this with an
* interface that sets the value as a property on the object that needs it.
*/
var ref: Ref<FocusNode>?
get() = null
set(value) {
value?.value = this
}
/**
* The recompose function of the Recompose component this [FocusNode] is hosted in.
*
* We need to trigger re-composition manually because we determine focus during composition, and
* editing an @Model object during composition does not trigger a re-composition.
*
* TODO (b/144897112): Remove manual recomposition.
*/
private lateinit var _recompose: () -> Unit
var recompose: () -> Unit
get() = _recompose
set(value) {
_recompose = value
}
/**
* The focus state for the current component. When the component is in the [Active] state, it
* receives key events and other actions. We use [FocusDetailedState]s internally and
* developers have the option to build their components using [FocusDetailedState], or a
* subset of states defined in [FocusState][androidx.ui.focus.FocusState].
*/
var focusState: FocusDetailedState = Inactive
internal set
/**
* The [LayoutCoordinates] of the [OnChildPositioned][androidx.ui.core.OnChildPositioned]
* component that hosts the child components of this [FocusNode].
*/
@Suppress("KDocUnresolvedReference")
var layoutCoordinates: LayoutCoordinates? = null
/**
* The list of focusable children of this [FocusNode]. The [ComponentNode] base class defines
* [children] of this node, but the [focusableChildren] set includes all the [FocusNode]s
* that are directly reachable from this [FocusNode].
*/
private val focusableChildren = mutableSetOf<FocusNode>()
/**
* The [FocusNode] from the set of [focusableChildren] that is currently [Active].
*/
private var focusedChild: FocusNode? = null
/**
* Add this focusable child to the parent's focusable children list.
*/
override fun attach(owner: Owner) {
findParentFocusNode()?.focusableChildren?.add(this)
super.attach(owner)
}
/**
* Remove this focusable child from the parent's focusable children list.
*/
override fun detach() {
// TODO (b/144119129): If this node is focused, let the parent know that it needs to
// grant focus to another focus node.
super.detach()
findParentFocusNode()?.focusableChildren?.remove(this)
}
/**
* Request focus for this node.
*
* @param propagateFocus Whether the focus should be propagated to the node's children.
*
* In Compose, the parent [FocusNode] controls focus for its focusable children.Calling this
* function will send a focus request to this [FocusNode]'s parent [FocusNode].
*/
fun requestFocus(propagateFocus: Boolean = true) {
when (focusState) {
Active, Captured, Disabled -> return
ActiveParent -> {
/** We don't need to do anything if [propagateFocus] is true,
since this subtree already has focus.*/
if (!propagateFocus && focusedChild?.clearFocus() ?: true) {
grantFocus(propagateFocus)
}
}
Inactive -> {
val focusParent = findParentFocusNode()
if (focusParent == null) {
// TODO (b/144116848) : Find out if the view hosting this composable is in focus.
// The top most focusable is [Active] only if the view hosting this composable is
// in focus. For now, we are making the assumption that our activity has only one
// view, and it is always in focus.
// Also, if the host AndroidComposeView does not have focus, request focus.
// Proceed to grant focus to this node only if the host view gains focus.
grantFocus(propagateFocus)
recompose()
} else {
focusParent.requestFocusForChild(this, propagateFocus)
}
}
}
}
/**
* Deny requests to clear focus.
*
* This is used when a component wants to hold onto focus (eg. A phone number field with an
* invalid number.
*
* @return true if the focus was successfully captured. False otherwise.
*/
fun captureFocus(): Boolean {
if (focusState == Active) {
focusState = Captured
return true
} else {
return false
}
}
/**
* When the node is in the [Captured] state, it rejects all requests to clear focus. Calling
* [freeFocus] puts the node in the [Active] state, where it is no longer preventing other
* nodes from requesting focus.
*
* @return true if the captured focus was released. If the node is not in the [Captured]
* state. this function returns false to indicate that this operation was a no-op.
*/
fun freeFocus(): Boolean {
if (focusState == Captured) {
focusState = Active
return true
} else {
return false
}
}
/**
* This function grants focus to this node.
*
* @param propagateFocus Whether the focus should be propagated to the node's children.
*
* Note: This function is private, and should only be called by a parent [FocusNode] to grant
* focus to one of its child [FocusNode]s.
*/
private fun grantFocus(propagateFocus: Boolean) {
// TODO (b/144126570) use ChildFocusablility.
// For now we assume children get focus before parent).
// TODO (b/144126759): Design a system to decide which child get's focus.
// for now we grant focus to the first child.
val focusedCandidate = focusableChildren.firstOrNull()
if (focusedCandidate == null || !propagateFocus) {
// No Focused Children, or we don't want to propagate focus to children.
focusState = Active
} else {
focusState = ActiveParent
focusedChild = focusedCandidate
focusedCandidate.grantFocus(propagateFocus)
focusedCandidate.recompose()
}
}
/**
* This function clears focus from this node.
*
* Note: This function is private, and should only be called by a parent [FocusNode] to clear
* focus from one of its child [FocusNode]s.
*/
private fun clearFocus(): Boolean {
return when (focusState) {
Active -> {
focusState = Inactive
true
}
/**
* If the node is [ActiveParent], we need to clear focus from the [Active] descendant
* first, before clearing focus of this node.
*/
ActiveParent -> focusedChild?.clearFocus() ?: error("No Focused Child")
/**
* If the node is [Captured], deny requests to clear focus.
*/
Captured -> false
/**
* Nothing to do if the node is not focused. Even though the node ends up in a
* cleared state, we return false to indicate that we didn't change any state (This
* return value is used to trigger a recomposition, so returning false will not
* trigger any recomposition).
*/
Inactive, Disabled -> false
}
}
/**
* Focusable children of this [FocusNode] can use this function to request focus.
*
* @param childNode: The node that is requesting focus.
* @param propagateFocus Whether the focus should be propagated to the node's children.
* @return true if focus was granted, false otherwise.
*/
private fun requestFocusForChild(childNode: FocusNode, propagateFocus: Boolean): Boolean {
// Only this node's children can ask for focus.
if (!focusableChildren.contains(childNode)) {
error("Non child node cannot request focus.")
}
return when (focusState) {
/**
* If this node is [Active], it can give focus to the requesting child.
*/
Active -> {
focusState = ActiveParent
focusedChild = childNode
childNode.grantFocus(propagateFocus)
recompose()
true
}
/**
* If this node is [ActiveParent] ie, one of the parent's descendants is [Active],
* remove focus from the currently focused child and grant it to the requesting child.
*/
ActiveParent -> {
val previouslyFocusedNode = focusedChild ?: error("no focusedChild found")
if (previouslyFocusedNode.clearFocus()) {
focusedChild = childNode
childNode.grantFocus(propagateFocus)
previouslyFocusedNode.recompose()
childNode.recompose()
true
} else {
// Currently focused component does not want to give up focus.
false
}
}
/**
* If this node is not [Active], we must gain focus first before granting it
* to the requesting child.
*/
Inactive -> {
val focusParent = findParentFocusNode()
if (focusParent == null) {
requestFocusForOwner()
// If the owner successfully gains focus, proceed otherwise return false.
if (ownerHasFocus()) {
focusState = Active
requestFocusForChild(childNode, propagateFocus)
} else {
false
}
} else if (focusParent.requestFocusForChild(this, propagateFocus = false)) {
requestFocusForChild(childNode, propagateFocus)
} else {
// Could not gain focus, so have no focus to give.
false
}
}
/**
* If this node is [Captured], decline requests from the children.
*/
Captured -> false
/**
* Children of a [Disabled] parent should also be [Disabled].
*/
Disabled -> error("non root FocusNode needs a focusable parent")
}
}
}
/**
* Backing node for the Draw component.
*/
class DrawNode : ComponentNode() {
var onPaintWithChildren: (ContentDrawScope.(canvas: Canvas, parentSize: PxSize) -> Unit)? = null
set(value) {
field = value
invalidate()
}
var onPaint: (Density.(canvas: Canvas, parentSize: PxSize) -> Unit)? = null
set(value) {
field = value
invalidate()
}
var needsPaint = false
override fun attach(owner: Owner) {
super.attach(owner)
needsPaint = true
owner.onInvalidate(this)
}
override fun detach() {
invalidate()
super.detach()
needsPaint = false
}
fun invalidate() {
if (!needsPaint) {
needsPaint = true
owner?.onInvalidate(this)
}
}
}
/**
* Backing node for Layout component.
*
* Measuring a [LayoutNode] as a [Measurable] will measure the node's content as adjusted by
* [modifier].
*/
class LayoutNode : ComponentNode(), Measurable {
interface MeasureBlocks {
/**
* The function used to measure the child. It must call [MeasureScope.layout] before
* completing.
*/
fun measure(
measureScope: MeasureScope,
measurables: List<Measurable>,
constraints: Constraints,
layoutDirection: LayoutDirection
): MeasureScope.MeasureResult
/**
* The function used to calculate [IntrinsicMeasurable.minIntrinsicWidth].
*/
fun minIntrinsicWidth(
density: Density,
measurables: List<IntrinsicMeasurable>,
h: IntPx,
layoutDirection: LayoutDirection
): IntPx
/**
* The lambda used to calculate [IntrinsicMeasurable.minIntrinsicHeight].
*/
fun minIntrinsicHeight(
density: Density,
measurables: List<IntrinsicMeasurable>,
w: IntPx,
layoutDirection: LayoutDirection
): IntPx
/**
* The function used to calculate [IntrinsicMeasurable.maxIntrinsicWidth].
*/
fun maxIntrinsicWidth(
density: Density,
measurables: List<IntrinsicMeasurable>,
h: IntPx,
layoutDirection: LayoutDirection
): IntPx
/**
* The lambda used to calculate [IntrinsicMeasurable.maxIntrinsicHeight].
*/
fun maxIntrinsicHeight(
density: Density,
measurables: List<IntrinsicMeasurable>,
w: IntPx,
layoutDirection: LayoutDirection
): IntPx
}
abstract class NoIntrinsicsMeasureBlocks(private val error: String) : MeasureBlocks {
override fun minIntrinsicWidth(
density: Density,
measurables: List<IntrinsicMeasurable>,
h: IntPx,
layoutDirection: LayoutDirection
) = error(error)
override fun minIntrinsicHeight(
density: Density,
measurables: List<IntrinsicMeasurable>,
w: IntPx,
layoutDirection: LayoutDirection
) = error(error)
override fun maxIntrinsicWidth(
density: Density,
measurables: List<IntrinsicMeasurable>,
h: IntPx,
layoutDirection: LayoutDirection
) = error(error)
override fun maxIntrinsicHeight(
density: Density,
measurables: List<IntrinsicMeasurable>,
w: IntPx,
layoutDirection: LayoutDirection
) = error(error)
}
// TODO(popam): used for multi composable children. Consider removing if possible.
abstract class InnerMeasureScope : MeasureScope() {
abstract val layoutNode: LayoutNode
}
/**
* Blocks that define the measurement and intrinsic measurement of the layout.
*/
var measureBlocks: MeasureBlocks = ErrorMeasureBlocks
set(value) {
if (field != value) {
field = value
requestRemeasure()
}
}
/**
* The scope used to run the [MeasureBlocks.measure] [MeasureBlock].
*/
val measureScope: MeasureScope = object : InnerMeasureScope(), Density {
private val ownerDensity: Density
get() = owner?.density ?: Density(1f)
override val density: Float
get() = ownerDensity.density
override val fontScale: Float
get() = ownerDensity.fontScale
override val layoutNode: LayoutNode = this@LayoutNode
}
/**
* The constraints used the last time [layout] was called.
*/
var constraints: Constraints = Constraints.fixed(IntPx.Zero, IntPx.Zero)
/**
* The layout direction of the layout node.
*/
var layoutDirection: LayoutDirection? = null
get() {
if (field == null) {
// root node doesn't have a parent but its LD value is set during root creation
field = parentLayoutNode?.layoutDirection
}
return field
}
/**
* Implementation oddity around composition; used to capture a reference to this
* [LayoutNode] when composed. This is a reverse property that mutates its right-hand side.
*/
var ref: Ref<LayoutNode>?
get() = null
set(value) {
value?.value = this
}
/**
* The measured width of this layout and all of its [modifier]s. Shortcut for `size.width`.
*/
val width: IntPx get() = layoutNodeWrapper.width
/**
* The measured height of this layout and all of its [modifier]s. Shortcut for `size.height`.
*/
val height: IntPx get() = layoutNodeWrapper.height
/**
* The alignment lines of this layout, inherited + intrinsic
*/
internal val alignmentLines: MutableMap<AlignmentLine, IntPx> = hashMapOf()
/**
* The alignment lines provided by this layout at the last measurement
*/
internal val providedAlignmentLines: MutableMap<AlignmentLine, IntPx> = hashMapOf()
/**
* Whether or not this has been placed in the hierarchy.
*/
var isPlaced = false
internal set
/**
* `true` when the parent's size depends on this LayoutNode's size
*/
var affectsParentSize: Boolean = true
private set
/**
* `true` when inside [measure]
*/
var isMeasuring: Boolean = false
private set
/**
* `true` when inside [layout]
*/
var isLayingOut: Boolean = false
private set
/**
* `true` when the current node is positioned during the measure pass,
* since it needs to compute alignment lines.
*/
var positionedDuringMeasurePass: Boolean = false
/**
* `true` when the layout has been dirtied by [requestRemeasure]. `false` after
* the measurement has been complete ([place] has been called).
*/
var needsRemeasure = false
internal set(value) {
require(!isMeasuring)
field = value
}
/**
* `true` when the layout has been measured or dirtied because the layout
* lambda accessed a model that has been dirtied.
*/
var needsRelayout = false
internal set(value) {
require(!isMeasuring)
require(!isLayingOut)
field = value
}
/**
* `true` when the parent reads our alignment lines
*/
internal var alignmentLinesRead = false
/**
* `true` when the alignment lines have to be recomputed because the layout has
* been remeasured
*/
internal var dirtyAlignmentLines = true
/**
* `true` when an ancestor relies on our alignment lines
*/
internal val alignmentLinesRequired
get() = alignmentLinesQueryOwner != null && alignmentLinesQueryOwner!!.alignmentLinesRead
/**
* Used by the parent to identify if the child has been queried for alignment lines since
* last measurement.
*/
internal var alignmentLinesQueriedSinceLastLayout = false
/**
* The closest layout node above in the hierarchy which asked for alignment lines.
*/
internal var alignmentLinesQueryOwner: LayoutNode? = null
override val parentLayoutNode: LayoutNode?
get() = super.containingLayoutNode
override val containingLayoutNode: LayoutNode?
get() = this
/**
* A local version of [Owner.measureIteration] to ensure that [MeasureBlocks.measure]
* is not called multiple times within a measure pass.
*/
internal var measureIteration = 0L
private set
@Deprecated("Temporary API to support ConstraintLayout prototyping.")
var canMultiMeasure: Boolean = false
/**
* Identifies when [layoutChildren] needs to be recalculated or if it can use
* the cached value.
*/
internal var layoutChildrenDirty = true
/**
* The cached value of [layoutChildren]
*/
private val _layoutChildren = mutableListOf<LayoutNode>()
/**
* All first level [LayoutNode] descendants. All LayoutNodes in the List
* will have this as [parentLayoutNode].
*/
val layoutChildren: List<LayoutNode>
get() {
if (layoutChildrenDirty) {
_layoutChildren.clear()
addLayoutChildren(this, _layoutChildren)
layoutChildrenDirty = false
}
return _layoutChildren
}
/**
* `true` when parentDataNode has to be rediscovered. This is when the
* LayoutNode has been attached.
*/
private var parentDataDirty = false
override val parentData: Any?
get() = layoutNodeWrapper.parentData
/**
* The parentData [DataNode] for this LayoutNode.
*/
internal var parentDataNode: DataNode<*>? = null
get() {
if (parentDataDirty) {
// walk up to find ParentData
field = null
var node = parent
val parentLayoutNode = parentLayoutNode
while (node != null && node !== parentLayoutNode) {
if (node is DataNode<*> && node.key === ParentDataKey) {
field = node
break
}
node = node.parent
}
parentDataDirty = false
}
return field
}
internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
internal var layoutNodeWrapper = innerLayoutNodeWrapper
/**
* zIndex defines the drawing order of the LayoutNode. Children with larger zIndex are drawn
* after others (the original order is used for the nodes with the same zIndex).
* Default zIndex is 0. Current implementation is using the first(front) DrawLayerModifier's
* elevation as a zIndex. We will have a separate zIndex modifier later instead to decouple
* this features.
*/
internal val zIndex: Float get() = outerLayerModifier?.elevation ?: 0f
/**
* The outermost DrawLayerModifier in the modifier chain or `null` if there are no
* DrawLayerModifiers in the modifier chain.
*/
private var outerLayerModifier: DrawLayerModifier? = null
/**
* The [Modifier] currently applied to this node.
*/
var modifier: Modifier = Modifier
set(value) {
if (value == field) return
field = value
// Rebuild layoutNodeWrapper
val oldPlaceable = layoutNodeWrapper
val addedCallback = hasNewPositioningCallback()
onPositionedCallbacks.clear()
onChildPositionedCallbacks.clear()
outerLayerModifier = null
layoutNodeWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
var wrapper = toWrap
// The order in which the following blocks occur matters. For example, the
// DrawModifier block should be before the LayoutModifier block so that a Modifier
// that implements both DrawModifier and LayoutModifier will have it's draw bounds
// reflect the dimensions defined by the LayoutModifier.
if (mod is OnPositionedModifier) {
onPositionedCallbacks += mod
}
if (mod is OnChildPositionedModifier) {
onChildPositionedCallbacks += mod
}
if (mod is DrawModifier) {
wrapper = ModifiedDrawNode(wrapper, mod)
}
if (mod is DrawLayerModifier) {
wrapper = LayerWrapper(wrapper, mod)
outerLayerModifier = mod
}
if (mod is FocusModifier) {
require(mod is FocusModifierImpl)
wrapper = ModifiedFocusNode(wrapper, mod).also { mod.focusNode = it }
}
if (mod is PointerInputModifier) {
wrapper = PointerInputDelegatingWrapper(wrapper, mod)
}
@Suppress("Deprecation")
if (mod is LayoutModifier) {
wrapper = ModifiedLayoutNode(wrapper, mod)
}
if (mod is LayoutModifier2) {
wrapper = ModifiedLayoutNode2(wrapper, mod)
}
if (mod is ParentDataModifier) {
wrapper = ModifiedParentDataNode(wrapper, mod)
}
if (mod is SemanticsModifier) {
wrapper = SemanticsWrapper(wrapper, mod)
}
wrapper
}
// Optimize the case where the layout itself is not modified. A common reason for
// this is if no wrapping actually occurs above because no LayoutModifiers are
// present in the modifier chain.
if (oldPlaceable != layoutNodeWrapper) {
oldPlaceable.detach()
requestRemeasure()
} else if (!needsRemeasure && !needsRelayout && addedCallback) {
// We need to notify the callbacks of a change in position since there's
// a new one.
requestRemeasure()
}
val containing = parentLayoutNode
if (containing != null) {
layoutNodeWrapper.wrappedBy = containing.innerLayoutNodeWrapper
}
owner?.onInvalidate(this)
}
@Deprecated(
"Temporary API to support our transition from single child composables to modifiers."
)
// TODO(popam): remove this
var handlesParentData: Boolean = true
/**
* Coordinates of just the contents of the LayoutNode, after being affected by all modifiers.
*/
// TODO(mount): remove this
val coordinates: LayoutCoordinates
get() = innerLayoutNodeWrapper
override fun attach(owner: Owner) {
super.attach(owner)
requestRemeasure()
parentDataDirty = true
parentLayoutNode?.layoutChildrenDirty = true
onAttach?.invoke(owner)
}
/**
* Callback to be executed whenever the [LayoutNode] is attached to a new [Owner].
*/
var onAttach: ((Owner) -> Unit)? = null
override fun detach() {
val owner = owner!!
val parentLayoutNode = parentLayoutNode
if (parentLayoutNode != null) {
parentLayoutNode.onInvalidate()
parentLayoutNode.layoutChildrenDirty = true
parentLayoutNode.requestRemeasure()
}
parentDataDirty = true
alignmentLinesQueryOwner = null
onDetach?.invoke(owner)
layoutNodeWrapper.detach()
super.detach()
}
/**
* Callback to be executed whenever the [LayoutNode] is detached from an [Owner].
*/
var onDetach: ((Owner) -> Unit)? = null
/**
* List of all OnPositioned callbacks in the modifier chain.
*/
private val onPositionedCallbacks = mutableListOf<OnPositionedModifier>()
/**
* List of all OnChildPositioned callbacks in the modifier chain.
*/
private val onChildPositionedCallbacks = mutableListOf<OnChildPositionedModifier>()
override fun measure(constraints: Constraints): Placeable {
val owner = requireOwner()
val iteration = owner.measureIteration
@Suppress("Deprecation")
canMultiMeasure = canMultiMeasure ||
(parentLayoutNode != null && parentLayoutNode!!.canMultiMeasure)
@Suppress("Deprecation")
check(measureIteration != iteration || canMultiMeasure) {
"measure() may not be called multiple times on the same Measurable"
}
measureIteration = iteration
val parent = parentLayoutNode
// The more idiomatic, `if (parentLayoutNode?.isMeasuring == true)` causes boxing
affectsParentSize = parent != null && parent.isMeasuring == true
if (this.constraints == constraints && !needsRemeasure) {
return layoutNodeWrapper // we're already measured to this size, don't do anything
}
needsRemeasure = false
isMeasuring = true
dirtyAlignmentLines = true
this.constraints = constraints
owner.observeMeasureModelReads(this) {
layoutNodeWrapper.measure(constraints)
}
isMeasuring = false
needsRelayout = true
return layoutNodeWrapper
}
override fun minIntrinsicWidth(height: IntPx): IntPx =
layoutNodeWrapper.minIntrinsicWidth(height)
override fun maxIntrinsicWidth(height: IntPx): IntPx =
layoutNodeWrapper.maxIntrinsicWidth(height)
override fun minIntrinsicHeight(width: IntPx): IntPx =
layoutNodeWrapper.minIntrinsicHeight(width)
override fun maxIntrinsicHeight(width: IntPx): IntPx =
layoutNodeWrapper.maxIntrinsicHeight(width)
fun place(x: IntPx, y: IntPx) {
with(Placeable.PlacementScope) {
layoutNodeWrapper.place(x, y)
}
}
fun draw(canvas: Canvas) = layoutNodeWrapper.draw(canvas)
/**
* Carries out a hit test on the [PointerInputModifier]s associated with this [LayoutNode] and
* all [PointerInputModifier]s on all descendant [LayoutNode]s.
*
* If [pointerPositionRelativeToScreen] is within the bounds of any tested
* [PointerInputModifier]s, the [PointerInputModifier] is added to [hitPointerInputFilters]
* and true is returned.
*
* @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.
*
* @return True if any [PointerInputFilter]s were hit and thus added to
* [hitPointerInputFilters].
*/
fun hitTest(
pointerPositionRelativeToScreen: PxPosition,
hitPointerInputFilters: MutableList<PointerInputFilter>
): Boolean {
return layoutNodeWrapper.hitTest(pointerPositionRelativeToScreen, hitPointerInputFilters)
}
/**
* Returns the alignment line value for a given alignment line without affecting whether
* the flag for whether the alignment line was read.
*/
fun getAlignmentLine(line: AlignmentLine): IntPx? {
val linePos = alignmentLines[line] ?: return null
var pos = PxPosition(linePos, linePos)
var wrapper = innerLayoutNodeWrapper
while (wrapper != layoutNodeWrapper) {
pos = wrapper.toParentPosition(pos)
wrapper = wrapper.wrappedBy!!
}
pos = wrapper.toParentPosition(pos)
return if (line is HorizontalAlignmentLine) pos.y.round() else pos.x.round()
}
/**
* Return true if there is a new [OnPositionedModifier] or [OnChildPositionedModifier]
* assigned to this Layout.
*/
private fun hasNewPositioningCallback(): Boolean {
return modifier.foldOut(false) { mod, hasNewCallback ->
hasNewCallback || when (mod) {
is OnPositionedModifier -> mod !in onPositionedCallbacks
is OnChildPositionedModifier -> mod !in onChildPositionedCallbacks
else -> false
}
}
}
fun layout() {
if (needsRelayout) {
needsRelayout = false
isLayingOut = true
val owner = requireOwner()
owner.observeLayoutModelReads(this) {
layoutChildren.forEach { child ->
child.isPlaced = false
if (alignmentLinesRequired && child.dirtyAlignmentLines) {
child.needsRelayout = true
}
if (!child.alignmentLinesRequired) {
child.alignmentLinesQueryOwner = alignmentLinesQueryOwner
}
child.alignmentLinesQueriedSinceLastLayout = false
}
positionedDuringMeasurePass = parentLayoutNode?.isMeasuring ?: false ||
parentLayoutNode?.positionedDuringMeasurePass ?: false
innerLayoutNodeWrapper.measureResult.placeChildren(layoutDirection!!)
layoutChildren.forEach { child ->
child.alignmentLinesRead = child.alignmentLinesQueriedSinceLastLayout
}
}
if (alignmentLinesRequired && dirtyAlignmentLines) {
alignmentLines.clear()
layoutChildren.forEach { child ->
if (!child.isPlaced) return@forEach
child.alignmentLines.keys.forEach { childLine ->
val linePositionInContainer = child.getAlignmentLine(childLine)!!
// If the line was already provided by a previous child, merge the values.
alignmentLines[childLine] = if (childLine in alignmentLines) {
childLine.merge(
alignmentLines.getValue(childLine),
linePositionInContainer
)
} else {
linePositionInContainer
}
}
}
alignmentLines += providedAlignmentLines
dirtyAlignmentLines = false
}
isLayingOut = false
}
}
internal fun calculateAlignmentLines(): Map<AlignmentLine, IntPx> {
alignmentLinesRead = true
alignmentLinesQueryOwner = this
alignmentLinesQueriedSinceLastLayout = true
if (dirtyAlignmentLines) {
needsRelayout = true
layout()
}
return alignmentLines
}
internal fun handleMeasureResult(measureResult: MeasureScope.MeasureResult) {
innerLayoutNodeWrapper.measureResult = measureResult
this.providedAlignmentLines.clear()
this.providedAlignmentLines += measureResult.alignmentLines
}
/**
* Used to request a new measurement + layout pass from the owner.
*/
fun requestRemeasure() {
owner?.onRequestMeasure(this)
}
/**
* Used to request a new draw pass from the owner.
*/
fun onInvalidate() {
owner?.onInvalidate(this)
}
/**
* Execute your code within the [block] if you want some code to not be observed for the
* model reads even if you are currently inside some observed scope like measuring.
*/
fun ignoreModelReads(block: () -> Unit) {
requireOwner().pauseModelReadObserveration(block)
}
internal fun dispatchOnPositionedCallbacks() {
if (needsRelayout) {
return // it hasn't been properly positioned, so don't make a call
}
if (!isPlaced) {
return // it hasn't been placed, so don't make a call
}
// There are two types of callbacks:
// a) when the Layout is positioned - `onPositioned`
// b) when the child of the Layout is positioned - `onChildPositioned`
onPositionedCallbacks.fastForEach { it.onPositioned(coordinates) }
parentLayoutNode?.onChildPositionedCallbacks?.fastForEach {
it.onChildPositioned(coordinates)
}
// iterate through the subtree
layoutChildren.fastForEach { it.dispatchOnPositionedCallbacks() }
}
/**
* This returns a new List of Modifiers and the coordinates and any extra information
* that may be useful. This is used for tooling to retrieve layout modifier and layer
* information.
*/
fun getModifierInfo(): List<ModifierInfo> {
val infoList = mutableListOf<ModifierInfo>()
var wrapper = layoutNodeWrapper
while (wrapper != innerLayoutNodeWrapper) {
val info = if (wrapper is LayerWrapper) {
ModifierInfo(wrapper.modifier, wrapper, wrapper.layer)
} else {
wrapper as DelegatingLayoutNodeWrapper<*>
ModifierInfo(wrapper.modifier, wrapper)
}
infoList += info
wrapper = wrapper.wrapped!!
}
return infoList
}
override fun toString(): String {
return "${super.toString()} measureBlocks: $measureBlocks"
}
internal companion object {
private val ErrorMeasureBlocks = object : NoIntrinsicsMeasureBlocks(
error = "Undefined intrinsics block and it is required"
) {
override fun measure(
measureScope: MeasureScope,
measurables: List<Measurable>,
constraints: Constraints,
layoutDirection: LayoutDirection
) = error("Undefined measure and it is required")
}
private fun addLayoutChildren(node: ComponentNode, list: MutableList<LayoutNode>) {
node.visitChildren { child ->
if (child is LayoutNode) {
list += child
} else {
addLayoutChildren(child, list)
}
}
}
}
}
/**
* Used by tooling to examine the modifiers on a [LayoutNode].
*/
class ModifierInfo(
val modifier: Modifier,
val coordinates: LayoutCoordinates,
val extra: Any? = null
)
/**
* The key used in DataNode.
*
* @param T Identifies the type used in the value
* @property name A unique name identifying the type of the key.
*/
// TODO(mount): Make this inline
class DataNodeKey<T>(val name: String)
/**
* A ComponentNode that stores a value in the emitted hierarchy
*
* @param T The type used for the value
* @property key The key object used to identify the key
* @property value The value of the data being stored in the hierarchy
*/
class DataNode<T>(val key: DataNodeKey<T>, var value: T) : ComponentNode() {
override fun attach(owner: Owner) {
super.attach(owner)
parentLayoutNode?.requestRemeasure()
}
}
/**
* Returns [ComponentNode.owner] or throws if it is null.
*/
fun ComponentNode.requireOwner(): Owner = owner ?: ErrorMessages.NodeShouldBeAttached.state()
/**
* Inserts a child [ComponentNode] at a last index. If this ComponentNode [isAttached]
* then [child] will become [isAttached]ed also. [child] must have a `null` [ComponentNode.parent].
*/
fun ComponentNode.add(child: ComponentNode) {
insertAt(count, child)
}
/**
* Executes [block] on first level of [LayoutNode] descendants of this ComponentNode.
*/
fun ComponentNode.visitLayoutChildren(block: (LayoutNode) -> Unit) {
visitChildren { child ->
if (child is LayoutNode) {
block(child)
} else {
child.visitLayoutChildren(block)
}
}
}
/**
* Executes [block] on first level of [LayoutNode] descendants of this ComponentNode
* and returns the last `LayoutNode` to return `true` from [block].
*/
fun ComponentNode.findLastLayoutChild(block: (LayoutNode) -> Boolean): LayoutNode? {
for (i in count - 1 downTo 0) {
val child = this[i]
if (child is LayoutNode) {
if (block(child)) {
return child
}
} else {
val layoutNode = child.findLastLayoutChild(block)
if (layoutNode != null) {
return layoutNode
}
}
}
return null
}
/**
* Executes [selector] on every parent of this [ComponentNode] and returns the closest
* [ComponentNode] to return `true` from [selector] or null if [selector] returns false
* for all ancestors.
*/
fun ComponentNode.findClosestParentNode(selector: (ComponentNode) -> Boolean): ComponentNode? {
// TODO(b/143866294): move this to the testing side after the hierarchy isn't flattened anymore
var currentParent = parent
while (currentParent != null) {
if (selector(currentParent)) {
return currentParent
} else {
currentParent = currentParent.parent
}
}
return null
}
/**
* Returns `true` if this ComponentNode has no descendant [LayoutNode]s.
*/
fun ComponentNode.hasNoLayoutDescendants() = findLastLayoutChild { true } == null
/**
* DataNodeKey for ParentData
*/
val ParentDataKey = DataNodeKey<Any>("Compose:ParentData")