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