[go: nahoru, domu]

blob: 2e7985cf62eed91bfd6abed1fcca570d0801ca68 [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.semantics
import androidx.ui.core.AlignmentLine
import androidx.ui.core.ExperimentalLayoutNodeApi
import androidx.ui.core.LayoutNode
import androidx.ui.core.LayoutNodeWrapper
import androidx.ui.core.boundsInRoot
import androidx.ui.core.findClosestParentNode
import androidx.ui.core.globalBounds
import androidx.ui.core.globalPosition
import androidx.ui.semantics.AccessibilityAction
import androidx.ui.semantics.SemanticsPropertyKey
import androidx.ui.unit.IntSize
import androidx.ui.unit.PxBounds
import androidx.ui.geometry.Offset
import androidx.ui.util.fastForEach
/**
* Signature for a function that is called for each [SemanticsNode].
*
* Return false to stop visiting nodes.
*
* Used by [SemanticsNode.visitChildren].
*/
internal typealias SemanticsNodeVisitor = (node: SemanticsNode) -> Boolean
/**
* A node that represents some semantic data.
*/
@OptIn(ExperimentalLayoutNodeApi::class)
class SemanticsNode internal constructor(
/**
* The unique identifier for this node.
*
* The root node has an id of zero. Other nodes are given a unique id when
* they are created.
*/
val id: Int,
val unmergedConfig: SemanticsConfiguration,
// TODO(b/144404665): Testing currently mandates this be public - should it be?
var componentNode: LayoutNode
) {
private var dirty: Boolean = false
companion object {
// TODO(b/145955412) maybe randomize? don't want this to be a contract
// TODO: Might need to be atomic for multi-threaded composition
private var lastIdentifier: Int = 2
// TODO(b/145955062): This should be private, but needs to be accessed across modules
// (from framework)
fun generateNewId(): Int {
lastIdentifier += 1
return lastIdentifier
}
/**
* In tests use this function to reset the counter used to generate
* [SemanticsNode.id].
*/
internal fun debugResetSemanticsIdCounter() {
lastIdentifier = 0
}
}
/**
* Creates a semantic node.
*
* Each semantic node has a unique identifier that is assigned when the node
* is created.
*/
internal constructor(unmergedConfig: SemanticsConfiguration, layoutNode: LayoutNode) :
this(generateNewId(), unmergedConfig, layoutNode)
// GEOMETRY
/** The size of the bounding box for this node */
val size: IntSize
get() {
return componentNode.coordinates.size
}
/** The bounding box for this node relative to the root of this Compose hierarchy */
val boundsInRoot: PxBounds
get() {
return componentNode.coordinates.boundsInRoot
}
val globalBounds: PxBounds
get() {
return componentNode.coordinates.globalBounds
}
val globalPosition: Offset
get() {
return componentNode.coordinates.globalPosition
}
/**
* The merged configuration of this node
*/
// TODO(aelias): This is too expensive for a val (full subtree recreation every call);
// optimize this when the merging algorithm is improved.
val config: SemanticsConfiguration
get() {
return buildMergedConfig()
}
/**
* Returns the position of an [alignment line][AlignmentLine], or [AlignmentLine.Unspecified]
* if the line is not provided.
*/
fun getAlignmentLinePosition(line: AlignmentLine): Int {
return componentNode.coordinates[line]
}
private fun buildMergedConfig(
parentNode: SemanticsNode? = null,
mergedConfigFromParent: SemanticsConfiguration? = null,
mergeAllChildren: Boolean = false
): SemanticsConfiguration {
// The forced merging might not start at the top-level node,
// so we need to check at each level
@Suppress("NAME_SHADOWING")
val mergeAllChildren = mergeAllChildren || mergeAllDescendantsIntoThisNode
val mergedConfig: SemanticsConfiguration
if (mergedConfigFromParent == null || parentNode == null) {
// Start by copying our configuration so that we can add
// our children's configuration to it
mergedConfig = unmergedConfig.copy()
} else {
// We're being merged into our parent - add our node's data
mergedConfig = mergedConfigFromParent
// If we are forcibly merging, we want to ignore conflicts
mergedConfig.absorb(unmergedConfig, ignoreAlreadySet = mergeAllChildren)
}
if (!mergeAllChildren) {
return mergedConfig
}
// If we're merging children, then collect semantic information here.
// Order is significant here because we will attempt to merge duplicate keys.
// This affects, for instance, the label text.
for (child in unmergedChildren()) {
// Recursively walk down the tree and collect child data
child.buildMergedConfig(
parentNode = this,
mergedConfigFromParent = mergedConfig,
mergeAllChildren = mergeAllChildren
)
}
return mergedConfig
}
/** Whether this node and all of its descendants should be treated as one logical entity. */
private val mergeAllDescendantsIntoThisNode: Boolean
get() = unmergedConfig.isMergingSemanticsOfDescendants
// CHILDREN
private fun unmergedChildren(): List<SemanticsNode> {
val unmergedChildren: MutableList<SemanticsNode> = mutableListOf()
val semanticsChildren = componentNode.findOneLayerOfSemanticsWrappers()
semanticsChildren.fastForEach { semanticsChild ->
unmergedChildren.add(semanticsChild.semanticsNode())
}
return unmergedChildren
}
/** Contains the children in inverse hit test order (i.e. paint order). */
// TODO(aelias): This is too expensive for a val (full subtree recreation every call);
// optimize this when the merging algorithm is improved.
val children: List<SemanticsNode>
get() {
if (mergeAllDescendantsIntoThisNode) {
// All of our descendants will be merged, so we have no children after merging
return emptyList()
}
return unmergedChildren()
}
/** Whether this node has a non-zero number of children. */
val hasChildren
get() = children.isNotEmpty()
/**
* Visits the immediate children of this node.
*
* This function calls visitor for each immediate child until visitor returns
* false.
*/
private fun visitChildren(visitor: SemanticsNodeVisitor) {
children.fastForEach {
if (!visitor(it)) {
return
}
}
}
/**
* Visit all the descendants of this node.
*
* This function calls visitor for each descendant in a pre-order traversal
* until visitor returns false. Returns true if all the visitor calls
* returned true, otherwise returns false.
*/
internal fun visitDescendants(visitor: SemanticsNodeVisitor): Boolean {
children.fastForEach {
if (!visitor(it) || !it.visitDescendants(visitor))
return false
}
return true
}
/**
* Whether this SemanticNode is the root of a tree or not
*/
val isRoot: Boolean
get() = parent == null
/** The parent of this node in the tree. */
// TODO(b/145947383): this needs to be the *merged* parent
val parent: SemanticsNode?
get() {
var node = componentNode.findClosestParentNode { it.outerSemantics != null }
return node?.outerSemantics?.semanticsNode()
}
internal fun <T : Function<Boolean>> canPerformAction(
action: SemanticsPropertyKey<AccessibilityAction<T>>
) =
this.config.contains(action)
}
/**
* Returns the outermost semantics node on a LayoutNode.
*/
@OptIn(ExperimentalLayoutNodeApi::class)
internal val LayoutNode.outerSemantics: SemanticsWrapper?
get() {
return (this as? LayoutNode)?.outerLayoutNodeWrapper?.nearestSemantics
}
/**
* Returns the nearest semantics wrapper starting from a LayoutNodeWrapper.
*/
internal val LayoutNodeWrapper.nearestSemantics: SemanticsWrapper?
get() {
var wrapper: LayoutNodeWrapper? = this
while (wrapper != null) {
if (wrapper is SemanticsWrapper) return wrapper
wrapper = wrapper.wrapped
}
return null
}
/**
* Executes [selector] on every parent of this [SemanticsNode] and returns the closest
* [SemanticsNode] to return `true` from [selector] or null if [selector] returns false
* for all ancestors.
*/
fun SemanticsNode.findClosestParentNode(selector: (SemanticsNode) -> Boolean): SemanticsNode? {
var currentParent = parent
while (currentParent != null) {
if (selector(currentParent)) {
return currentParent
} else {
currentParent = currentParent.parent
}
}
return null
}
internal fun SemanticsNode.findChildById(id: Int): SemanticsNode? {
if (this.id == id) return this
children.fastForEach {
val result = it.findChildById(id)
if (result != null) return result
}
return null
}
@OptIn(ExperimentalLayoutNodeApi::class)
private fun LayoutNode.findOneLayerOfSemanticsWrappers(): List<SemanticsWrapper> {
val childSemanticsLayoutNodes = mutableListOf<SemanticsWrapper>()
children.fastForEach { child ->
findOneLayerOfSemanticsWrappersRecursive(childSemanticsLayoutNodes, child)
}
return childSemanticsLayoutNodes
}
@OptIn(ExperimentalLayoutNodeApi::class)
private fun LayoutNode.findOneLayerOfSemanticsWrappersRecursive(
list: MutableList<SemanticsWrapper>,
node: LayoutNode
) {
if (node.outerSemantics != null) {
list.add(node.outerSemantics!!)
// Stop, we're done
} else {
node.children.fastForEach { child ->
findOneLayerOfSemanticsWrappersRecursive(list, child)
}
}
}