Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move getAllUncoveredSemanticsNodesToIntObjectMap to common #1781

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,22 @@ import androidx.compose.ui.internal.checkPreconditionNotNull
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.platform.SemanticsNodeCopy
import androidx.compose.ui.platform.SemanticsNodeWithAdjustedBounds
import androidx.compose.ui.platform.coreshims.ContentCaptureSessionCompat
import androidx.compose.ui.platform.coreshims.ViewCompatShims
import androidx.compose.ui.platform.coreshims.ViewStructureCompat
import androidx.compose.ui.platform.getAllUncoveredSemanticsNodesToIntObjectMap
import androidx.compose.ui.platform.getTextLayoutResult
import androidx.compose.ui.platform.toLegacyClassName
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsNodeWithAdjustedBounds
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getAllUncoveredSemanticsNodesToIntObjectMap
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastJoinToString
import androidx.compose.ui.util.fastMap
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import java.util.function.Consumer
Expand Down Expand Up @@ -109,7 +110,10 @@ internal class AndroidContentCaptureManager(
get() {
if (currentSemanticsNodesInvalidated) { // first instance of retrieving all nodes
currentSemanticsNodesInvalidated = false
field = view.semanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap()
field =
view.semanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap(
customRootNodeId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
)
currentSemanticsNodesSnapshotTimestampMillis = System.currentTimeMillis()
}
return field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ import androidx.compose.ui.contentcapture.ContentCaptureManager
import androidx.compose.ui.focus.FocusDirection.Companion.Exit
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.internal.checkPreconditionNotNull
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.positionInRoot
Expand All @@ -84,9 +83,13 @@ import androidx.compose.ui.semantics.SemanticsActions.PageRight
import androidx.compose.ui.semantics.SemanticsActions.PageUp
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsNodeWithAdjustedBounds
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.SemanticsPropertiesAndroid
import androidx.compose.ui.semantics.getAllUncoveredSemanticsNodesToIntObjectMap
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.semantics.isHidden
import androidx.compose.ui.semantics.isImportantForAccessibility
import androidx.compose.ui.semantics.subtreeSortedByGeometryGrouping
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.AnnotatedString
Expand All @@ -95,6 +98,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.platform.URLSpanCache
import androidx.compose.ui.text.platform.toAccessibilitySpannableString
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toRect
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastForEach
Expand Down Expand Up @@ -310,7 +314,10 @@ internal class AndroidComposeViewAccessibilityDelegateCompat(val view: AndroidCo
get() {
if (currentSemanticsNodesInvalidated) { // first instance of retrieving all nodes
currentSemanticsNodesInvalidated = false
field = view.semanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap()
field =
view.semanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap(
customRootNodeId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
)
if (isEnabled) {
setTraversalValues(field, idToBeforeMap, idToAfterMap, view.context.resources)
}
Expand Down Expand Up @@ -402,7 +409,7 @@ internal class AndroidComposeViewAccessibilityDelegateCompat(val view: AndroidCo
// avoid overlapping siblings. Because position is a float (touch event can happen in-
// between pixels), convert the int-based Android Rect to a float-based Compose Rect
// before doing the comparison.
if (!node.adjustedBounds.toComposeRect().contains(position)) {
if (!node.adjustedBounds.toRect().contains(position)) {
return@forEachValue
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,19 @@
package androidx.compose.ui.platform

import android.annotation.SuppressLint
import android.graphics.Region
import android.view.View
import androidx.collection.IntObjectMap
import androidx.collection.MutableIntObjectMap
import androidx.collection.MutableIntSet
import androidx.collection.emptyIntObjectMap
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.node.OwnerScope
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.ScrollAxisRange
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.SemanticsProperties.HideFromAccessibility
import androidx.compose.ui.semantics.SemanticsProperties.InvisibleToUser
import androidx.compose.ui.semantics.SemanticsNodeWithAdjustedBounds
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastRoundToInt
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat

/**
* A snapshot of the semantics node. The children here is fixed and are taken from the time this
Expand Down Expand Up @@ -128,130 +119,6 @@ internal fun Role.toLegacyClassName(): String? =
else -> null
}

internal fun SemanticsNode.isImportantForAccessibility() =
!isHidden &&
(unmergedConfig.isMergingSemanticsOfDescendants ||
unmergedConfig.containsImportantForAccessibility())

@Suppress("DEPRECATION")
internal val SemanticsNode.isHidden: Boolean
// A node is considered hidden if it is transparent, or explicitly is hidden from accessibility.
// This also checks if the node has been marked as `invisibleToUser`, which is what the
// `hiddenFromAccessibility` API used to be named.
get() =
isTransparent ||
(unmergedConfig.contains(HideFromAccessibility) ||
unmergedConfig.contains(InvisibleToUser))

internal val DefaultFakeNodeBounds = Rect(0f, 0f, 10f, 10f)

/** Semantics node with adjusted bounds for the uncovered(by siblings) part. */
internal class SemanticsNodeWithAdjustedBounds(
val semanticsNode: SemanticsNode,
val adjustedBounds: android.graphics.Rect
)

/** This function retrieves the View corresponding to a semanticsId, if it exists. */
internal fun AndroidViewsHandler.semanticsIdToView(id: Int): View? =
layoutNodeToHolder.entries.firstOrNull { it.key.semanticsId == id }?.value

/**
* Finds pruned [SemanticsNode]s in the tree owned by this [SemanticsOwner]. A semantics node
* completely covered by siblings drawn on top of it will be pruned. Return the results in a map.
*/
internal fun SemanticsOwner.getAllUncoveredSemanticsNodesToIntObjectMap():
IntObjectMap<SemanticsNodeWithAdjustedBounds> {
val root = unmergedRootSemanticsNode
if (!root.layoutNode.isPlaced || !root.layoutNode.isAttached) {
return emptyIntObjectMap()
}

// Default capacity chosen to accommodate common scenarios
val nodes = MutableIntObjectMap<SemanticsNodeWithAdjustedBounds>(48)

val unaccountedSpace =
with(root.boundsInRoot) {
Region(
left.fastRoundToInt(),
top.fastRoundToInt(),
right.fastRoundToInt(),
bottom.fastRoundToInt()
)
}

fun findAllSemanticNodesRecursive(currentNode: SemanticsNode, region: Region) {
val notAttachedOrPlaced =
!currentNode.layoutNode.isPlaced || !currentNode.layoutNode.isAttached
if (
(unaccountedSpace.isEmpty && currentNode.id != root.id) ||
(notAttachedOrPlaced && !currentNode.isFake)
) {
return
}
val touchBoundsInRoot = currentNode.touchBoundsInRoot
val left = touchBoundsInRoot.left.fastRoundToInt()
val top = touchBoundsInRoot.top.fastRoundToInt()
val right = touchBoundsInRoot.right.fastRoundToInt()
val bottom = touchBoundsInRoot.bottom.fastRoundToInt()

region.set(left, top, right, bottom)

val virtualViewId =
if (currentNode.id == root.id) {
AccessibilityNodeProviderCompat.HOST_VIEW_ID
} else {
currentNode.id
}
if (region.op(unaccountedSpace, Region.Op.INTERSECT)) {
nodes[virtualViewId] = SemanticsNodeWithAdjustedBounds(currentNode, region.bounds)
// Children could be drawn outside of parent, but we are using clipped bounds for
// accessibility now, so let's put the children recursion inside of this if. If later
// we decide to support children drawn outside of parent, we can move it out of the
// if block.
val children = currentNode.replacedChildren
for (i in children.size - 1 downTo 0) {
// Links in text nodes are semantics children. But for Android accessibility support
// we don't publish them to the accessibility services because they are exposed
// as UrlSpan/ClickableSpan spans instead
if (children[i].config.contains(SemanticsProperties.LinkTestMarker)) {
continue
}
findAllSemanticNodesRecursive(children[i], region)
}
if (currentNode.isImportantForAccessibility()) {
unaccountedSpace.op(left, top, right, bottom, Region.Op.DIFFERENCE)
}
} else {
if (currentNode.isFake) {
val parentNode = currentNode.parent
// use parent bounds for fake node
val boundsForFakeNode =
if (parentNode?.layoutInfo?.isPlaced == true) {
parentNode.boundsInRoot
} else {
DefaultFakeNodeBounds
}
nodes[virtualViewId] =
SemanticsNodeWithAdjustedBounds(
currentNode,
android.graphics.Rect(
boundsForFakeNode.left.fastRoundToInt(),
boundsForFakeNode.top.fastRoundToInt(),
boundsForFakeNode.right.fastRoundToInt(),
boundsForFakeNode.bottom.fastRoundToInt(),
)
)
} else if (virtualViewId == AccessibilityNodeProviderCompat.HOST_VIEW_ID) {
// Root view might have WRAP_CONTENT layout params in which case it will have zero
// bounds if there is no other content with semantics. But we need to always send
// the
// root view info as there are some other apps (e.g. Google Assistant) that depend
// on accessibility info
nodes[virtualViewId] = SemanticsNodeWithAdjustedBounds(currentNode, region.bounds)
}
}
}

findAllSemanticNodesRecursive(root, Region())
return nodes
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ import androidx.compose.ui.internal.checkPreconditionNotNull
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.platform.isHidden
import androidx.compose.ui.semantics.SemanticsActions.ScrollByOffset
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.SemanticsProperties.Disabled
import androidx.compose.ui.semantics.SemanticsProperties.VerticalScrollAxisRange
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.semantics.isHidden
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.roundToIntRect
import java.util.function.Consumer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2024 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.semantics

import android.graphics.Region
import androidx.compose.ui.graphics.toComposeIntRect
import androidx.compose.ui.unit.IntRect

/** Wrapper around platform-specific [android.graphics.Region] class */
private class SemanticRegionImpl : SemanticsRegion {
val region = Region()

override fun set(rect: IntRect) {
region.set(rect.left, rect.top, rect.right, rect.bottom)
}

override val bounds: IntRect
get() = region.bounds.toComposeIntRect()

override val isEmpty: Boolean
get() = region.isEmpty

override fun intersect(region: SemanticsRegion): Boolean {
return this.region.op((region as SemanticRegionImpl).region, Region.Op.INTERSECT)
}

override fun difference(rect: IntRect): Boolean {
return region.op(rect.left, rect.top, rect.right, rect.bottom, Region.Op.DIFFERENCE)
}
}

/** Builder that creates wrapper around platform-specific Region class */
internal actual fun SemanticsRegion(): SemanticsRegion = SemanticRegionImpl()
Loading