1 /*
<lambda>null2 * Copyright 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.ui.semantics
18
19 import androidx.collection.IntObjectMap
20 import androidx.collection.MutableIntObjectMap
21 import androidx.collection.intObjectMapOf
22 import androidx.collection.mutableIntObjectMapOf
23 import androidx.compose.ui.geometry.Rect
24 import androidx.compose.ui.node.LayoutNode
25 import androidx.compose.ui.unit.LayoutDirection
26 import androidx.compose.ui.util.fastForEach
27 import kotlin.math.max
28 import kotlin.math.min
29
30 /**
31 * This function prepares a subtree for `sortByGeometryGroupings` by retrieving all non-container
32 * nodes and adding them to the list to be geometrically sorted. We recurse on containers (if they
33 * exist) and add their sorted children to an optional mapping. The list to be sorted and child
34 * mapping is passed into `sortByGeometryGroupings`.
35 */
36 internal fun SemanticsNode.subtreeSortedByGeometryGrouping(
37 isVisible: (SemanticsNode) -> Boolean,
38 isFocusableContainer: (SemanticsNode) -> Boolean,
39 listToSort: List<SemanticsNode>
40 ): List<SemanticsNode> {
41 // This should be mapping of [containerID: listOfSortedChildren], only populated if there
42 // are container nodes in this level. If there are container nodes, `containerMapToChildren`
43 // would look like {containerId: [sortedChild, sortedChild], containerId: [sortedChild]}
44 val containerMapToChildren = mutableIntObjectMapOf<List<SemanticsNode>>()
45 val geometryList = ArrayList<SemanticsNode>()
46
47 listToSort.fastForEach { node ->
48 node.geometryDepthFirstSearch(
49 geometryList,
50 isVisible,
51 isFocusableContainer,
52 containerMapToChildren
53 )
54 }
55
56 return sortByGeometryGroupings(geometryList, isFocusableContainer, containerMapToChildren)
57 }
58
SemanticsNodenull59 private fun SemanticsNode.geometryDepthFirstSearch(
60 geometryList: ArrayList<SemanticsNode>,
61 isVisible: (SemanticsNode) -> Boolean,
62 isFocusableContainer: (SemanticsNode) -> Boolean,
63 containerMapToChildren: MutableIntObjectMap<List<SemanticsNode>>
64 ) {
65 // We only want to add children that are either traversalGroups or are
66 // screen reader focusable. The child must also be in the current pruned semantics tree.
67 val isTraversalGroup = unmergedConfig.getOrElse(SemanticsProperties.IsTraversalGroup) { false }
68
69 if ((isTraversalGroup || isFocusableContainer(this)) && isVisible(this)) {
70 geometryList.add(this)
71 }
72 if (isTraversalGroup) {
73 // Recurse and record the container's children, sorted
74 containerMapToChildren[id] =
75 subtreeSortedByGeometryGrouping(isVisible, isFocusableContainer, children)
76 } else {
77 // Otherwise, continue adding children to the list that'll be sorted regardless of hierarchy
78 children.fastForEach { child ->
79 child.geometryDepthFirstSearch(
80 geometryList,
81 isVisible,
82 isFocusableContainer,
83 containerMapToChildren
84 )
85 }
86 }
87 }
88
89 /**
90 * Returns the results of geometry groupings, which is determined from 1) grouping nodes into
91 * distinct, non-overlapping rows based on their top/bottom coordinates, then 2) sorting nodes
92 * within each row with the semantics comparator.
93 *
94 * This method approaches traversal order with more nuance than an approach considering only just
95 * hierarchy or only just an individual node's bounds.
96 *
97 * If [containerChildrenMapping] exists, there are additional children to add, as well as the sorted
98 * parent itself
99 */
sortByGeometryGroupingsnull100 internal fun SemanticsNode.sortByGeometryGroupings(
101 parentListToSort: List<SemanticsNode>,
102 isFocusableContainer: (SemanticsNode) -> Boolean = { false },
103 containerChildrenMapping: IntObjectMap<List<SemanticsNode>> = intObjectMapOf()
104 ): List<SemanticsNode> {
105 val layoutIsRtl = layoutInfo.layoutDirection == LayoutDirection.Rtl
106
107 // RowGroupings list consists of pairs, first = a rectangle of the bounds of the row
108 // and second = the list of nodes in that row
109 val rowGroupings = ArrayList<Pair<Rect, MutableList<SemanticsNode>>>(parentListToSort.size / 2)
110
111 for (entryIndex in 0..parentListToSort.lastIndex) {
112 val currEntry = parentListToSort[entryIndex]
113 // If this is the first entry, or vertical groups don't overlap
114 if (entryIndex == 0 || !placedEntryRowOverlaps(rowGroupings, currEntry)) {
115 val newRect = currEntry.boundsInWindow
116 rowGroupings.add(Pair(newRect, mutableListOf(currEntry)))
117 } // otherwise, we've already iterated through, found and placed it in a matching group
118 }
119
120 // Sort the rows from top to bottom
121 rowGroupings.sortWith(TopBottomBoundsComparator)
122
123 val returnList = ArrayList<SemanticsNode>()
124 val comparator = semanticComparators[if (layoutIsRtl) 0 else 1]
rownull125 rowGroupings.fastForEach { row ->
126 // Sort each individual row's parent nodes
127 row.second.sortWith(comparator)
128 returnList.addAll(row.second)
129 }
130
131 returnList.sortWith(UnmergedConfigComparator)
132
133 var i = 0
134 // Afterwards, go in and add the containers' children.
135 while (i <= returnList.lastIndex) {
136 val currNodeId = returnList[i].id
137 // If a parent node is a container, then add its children.
138 // Add all container's children after the container itself.
139 // Because we've already recursed on the containers children, the children should
140 // also be sorted by their traversal index
141 val containersChildrenList = containerChildrenMapping[currNodeId]
142 if (containersChildrenList != null) {
143 val containerIsScreenReaderFocusable = isFocusableContainer(returnList[i])
144 if (!containerIsScreenReaderFocusable) {
145 // Container is removed if it is not screenreader-focusable
146 returnList.removeAt(i)
147 } else {
148 // Increase counter if the container was not removed
149 i += 1
150 }
151 // Add all the container's children and increase counter by the number of children
152 returnList.addAll(i, containersChildrenList)
153 i += containersChildrenList.size
154 } else {
155 // Advance to the next item
156 i += 1
157 }
158 }
159 return returnList
160 }
161
162 // check to see if this entry overlaps with any groupings in rowGroupings
placedEntryRowOverlapsnull163 private fun placedEntryRowOverlaps(
164 rowGroupings: ArrayList<Pair<Rect, MutableList<SemanticsNode>>>,
165 node: SemanticsNode
166 ): Boolean {
167 // Conversion to long is needed in order to utilize `until`, which has no float ver
168 val entryTopCoord = node.boundsInWindow.top
169 val entryBottomCoord = node.boundsInWindow.bottom
170 val entryIsEmpty = entryTopCoord >= entryBottomCoord
171
172 for (currIndex in 0..rowGroupings.lastIndex) {
173 val currRect = rowGroupings[currIndex].first
174 val groupIsEmpty = currRect.top >= currRect.bottom
175 val groupOverlapsEntry =
176 !entryIsEmpty &&
177 !groupIsEmpty &&
178 max(entryTopCoord, currRect.top) < min(entryBottomCoord, currRect.bottom)
179
180 // If it overlaps with this row group, update cover and add node
181 if (groupOverlapsEntry) {
182 val newRect =
183 currRect.intersect(0f, entryTopCoord, Float.POSITIVE_INFINITY, entryBottomCoord)
184 // Replace the cover rectangle, copying over the old list of nodes
185 rowGroupings[currIndex] = Pair(newRect, rowGroupings[currIndex].second)
186 // Add current node
187 rowGroupings[currIndex].second.add(node)
188 // We've found an overlapping group, return true
189 return true
190 }
191 }
192
193 // If we've made it here, then there are no groups our entry overlaps with
194 return false
195 }
196
197 private val semanticComparators: Array<Comparator<SemanticsNode>> =
indexnull198 Array(2) { index ->
199 val comparator =
200 when (index) {
201 0 -> RtlBoundsComparator
202 else -> LtrBoundsComparator
203 }
204 comparator
205 // then compare by layoutNode's zIndex and placement order
206 .thenBy(LayoutNode.ZComparator) { it.layoutNode }
207 // then compare by semanticsId to break the tie somehow
208 .thenBy { it.id }
209 }
210
211 private object LtrBoundsComparator : Comparator<SemanticsNode> {
comparenull212 override fun compare(a: SemanticsNode, b: SemanticsNode): Int {
213 // TODO: boundsInWindow is quite expensive and allocates several objects,
214 // we need to fix this since this is called during sorting
215 val ab = a.boundsInWindow
216 val bb = b.boundsInWindow
217 var r = ab.left.compareTo(bb.left)
218 if (r != 0) return r
219 r = ab.top.compareTo(bb.top)
220 if (r != 0) return r
221 r = ab.bottom.compareTo(bb.bottom)
222 if (r != 0) return r
223 return ab.right.compareTo(bb.right)
224 }
225 }
226
227 private object RtlBoundsComparator : Comparator<SemanticsNode> {
comparenull228 override fun compare(a: SemanticsNode, b: SemanticsNode): Int {
229 // TODO: boundsInWindow is quite expensive and allocates several objects,
230 // we need to fix this since this is called during sorting
231 val ab = a.boundsInWindow
232 val bb = b.boundsInWindow
233 // We want to compare the right-most bounds, with the largest values first — that way
234 // the nodes will be sorted from right to left. Since `compareTo` returns a positive
235 // number if the first object is greater than the second, we want to call
236 // `b.compareTo(a)`, since we want our values in descending order, rather than
237 // ascending order.
238 var r = bb.right.compareTo(ab.right)
239 if (r != 0) return r
240 // Since in RTL layouts we still read from top to bottom, we compare the top and
241 // bottom bounds as usual.
242 r = ab.top.compareTo(bb.top)
243 if (r != 0) return r
244 r = ab.bottom.compareTo(bb.bottom)
245 if (r != 0) return r
246 // We also want to sort the left bounds in descending order, so call `b.compareTo(a)`
247 // here too.
248 return bb.left.compareTo(ab.left)
249 }
250 }
251
252 private object TopBottomBoundsComparator : Comparator<Pair<Rect, MutableList<SemanticsNode>>> {
comparenull253 override fun compare(
254 a: Pair<Rect, MutableList<SemanticsNode>>,
255 b: Pair<Rect, MutableList<SemanticsNode>>
256 ): Int {
257 val r = a.first.top.compareTo(b.first.top)
258 if (r != 0) return r
259 return a.first.bottom.compareTo(b.first.bottom)
260 }
261 }
262
263 // Kotlin `sortWith` should just pull out the highest traversal indices, but keep everything
264 // else in place. If the element does not have a `traversalIndex` then `0f` will be used.
anull265 private val UnmergedConfigComparator: (SemanticsNode, SemanticsNode) -> Int = { a, b ->
266 a.unmergedConfig
267 .getOrElse(SemanticsProperties.TraversalIndex) { 0f }
268 .compareTo(b.unmergedConfig.getOrElse(SemanticsProperties.TraversalIndex) { 0f })
269 }
270