1 /*
<lambda>null2 * Copyright 2021 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.platform.accessibility
18
19 import androidx.compose.ui.geometry.Offset
20 import androidx.compose.ui.semantics.CollectionInfo
21 import androidx.compose.ui.semantics.CollectionItemInfo
22 import androidx.compose.ui.semantics.SemanticsNode
23 import androidx.compose.ui.semantics.SemanticsProperties
24 import androidx.compose.ui.semantics.getOrNull
25 import androidx.compose.ui.util.fastForEach
26 import androidx.compose.ui.util.fastReduce
27 import androidx.compose.ui.util.fastZipWithNext
28 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
29 import kotlin.math.abs
30
31 internal fun setCollectionInfo(node: SemanticsNode, info: AccessibilityNodeInfoCompat) {
32 // prioritise collection info provided by developer
33 val collectionInfo = node.config.getOrNull(SemanticsProperties.CollectionInfo)
34 if (collectionInfo != null) {
35 info.setCollectionInfo(collectionInfo.toAccessibilityCollectionInfo())
36 return
37 }
38
39 // if no collection info is provided, we'll check the 'SelectableGroup'
40 val groupedChildren = mutableListOf<SemanticsNode>()
41
42 if (node.config.getOrNull(SemanticsProperties.SelectableGroup) != null) {
43 node.replacedChildren.fastForEach { childNode ->
44 // we assume that Tabs and RadioButtons are not mixed under a single group
45 if (childNode.config.contains(SemanticsProperties.Selected)) {
46 groupedChildren.add(childNode)
47 }
48 }
49 }
50
51 if (groupedChildren.isNotEmpty()) {
52 val isHorizontal = calculateIfHorizontallyStacked(groupedChildren)
53 info.setCollectionInfo(
54 AccessibilityNodeInfoCompat.CollectionInfoCompat.obtain(
55 if (isHorizontal) 1 else groupedChildren.count(),
56 if (isHorizontal) groupedChildren.count() else 1,
57 false,
58 AccessibilityNodeInfoCompat.CollectionInfoCompat.SELECTION_MODE_NONE
59 )
60 )
61 }
62 }
63
setCollectionItemInfonull64 internal fun setCollectionItemInfo(node: SemanticsNode, info: AccessibilityNodeInfoCompat) {
65 // prioritise collection item info provided by developer
66 val collectionItemInfo = node.config.getOrNull(SemanticsProperties.CollectionItemInfo)
67 if (collectionItemInfo != null) {
68 info.setCollectionItemInfo(collectionItemInfo.toAccessibilityCollectionItemInfo(node))
69 }
70
71 // if no collection item info is provided, we'll check the 'SelectableGroup'
72 val parentNode = node.parent ?: return
73 if (parentNode.config.getOrNull(SemanticsProperties.SelectableGroup) != null) {
74 // first check if parent has a CollectionInfo. If it does and any of the counters is
75 // unknown, then we assume that it is a lazy collection so we won't provide
76 // collectionItemInfo using `SelectableGroup`
77 val collectionInfo = parentNode.config.getOrNull(SemanticsProperties.CollectionInfo)
78 if (collectionInfo != null && collectionInfo.isLazyCollection) return
79
80 // `SelectableGroup` designed for selectable elements
81 if (!node.config.contains(SemanticsProperties.Selected)) return
82
83 val groupedChildren = mutableListOf<SemanticsNode>()
84
85 // find all siblings to calculate the index
86 var index = 0
87 parentNode.replacedChildren.fastForEach { childNode ->
88 if (childNode.config.contains(SemanticsProperties.Selected)) {
89 groupedChildren.add(childNode)
90 // Grouped children is ordered preferring zIndex
91 if (childNode.layoutNode.placeOrder < node.layoutNode.placeOrder) {
92 index++
93 }
94 }
95 }
96
97 if (groupedChildren.isNotEmpty()) {
98 val isHorizontal = calculateIfHorizontallyStacked(groupedChildren)
99 val itemInfo =
100 AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
101 if (isHorizontal) 0 else index,
102 1,
103 if (isHorizontal) index else 0,
104 1,
105 false,
106 node.config.getOrElse(SemanticsProperties.Selected) { false }
107 )
108 if (itemInfo != null) {
109 info.setCollectionItemInfo(itemInfo)
110 }
111 }
112 }
113 }
114
hasCollectionInfonull115 internal fun SemanticsNode.hasCollectionInfo() =
116 config.getOrNull(SemanticsProperties.CollectionInfo) != null ||
117 config.getOrNull(SemanticsProperties.SelectableGroup) != null
118
119 /** A naïve algorithm to determine if elements are stacked vertically or horizontally */
120 private fun calculateIfHorizontallyStacked(items: List<SemanticsNode>): Boolean {
121 if (items.count() < 2) return true
122
123 val deltas =
124 items.fastZipWithNext { el1, el2 ->
125 Offset(
126 abs(el1.boundsInRoot.center.x - el2.boundsInRoot.center.x),
127 abs(el1.boundsInRoot.center.y - el2.boundsInRoot.center.y)
128 )
129 }
130 val (deltaX, deltaY) =
131 when (deltas.count()) {
132 1 -> deltas.first()
133 else -> deltas.fastReduce { result, element -> result + element }
134 }
135 return deltaY < deltaX
136 }
137
138 private val CollectionInfo.isLazyCollection
139 get() = rowCount < 0 || columnCount < 0
140
toAccessibilityCollectionInfonull141 private fun CollectionInfo.toAccessibilityCollectionInfo() =
142 AccessibilityNodeInfoCompat.CollectionInfoCompat.obtain(
143 rowCount,
144 columnCount,
145 false,
146 AccessibilityNodeInfoCompat.CollectionInfoCompat.SELECTION_MODE_NONE
147 )
148
149 private fun CollectionItemInfo.toAccessibilityCollectionItemInfo(itemNode: SemanticsNode) =
150 AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
151 rowIndex,
152 rowSpan,
153 columnIndex,
154 columnSpan,
155 false,
156 itemNode.config.getOrElse(SemanticsProperties.Selected) { false }
157 )
158