1616
1717package androidx.compose.ui.platform.a11y
1818
19- import androidx.collection.mutableScatterMapOf
19+ import androidx.collection.mutableIntObjectMapOf
2020import androidx.compose.ui.platform.PlatformComponent
2121import androidx.compose.ui.semantics.ProgressBarRangeInfo
2222import androidx.compose.ui.semantics.SemanticsConfiguration
@@ -68,7 +68,7 @@ internal class SemanticsOwnerAccessibility(
6868 * Maps the [ComposeAccessible]s we have created by the [SemanticsNode.id] for which they were
6969 * created.
7070 */
71- private var accessibleByNodeId = mutableScatterMapOf< Int , ComposeAccessible >()
71+ private var accessibleByNodeId = mutableIntObjectMapOf< ComposeAccessible >()
7272
7373 /* *
7474 * Whether [accessibleByNodeId] is up to date.
@@ -117,7 +117,7 @@ internal class SemanticsOwnerAccessibility(
117117 * Invoked when a new [ComposeAccessible] is created.
118118 */
119119 private fun onNodeAdded (accessible : ComposeAccessible ) {
120- for (entry in accessible.semanticsNode.config ) {
120+ for (entry in accessible.semanticsConfig ) {
121121 when (entry.key) {
122122 SemanticsProperties .Focused -> {
123123 if (entry.value as Boolean ) {
@@ -147,7 +147,7 @@ internal class SemanticsOwnerAccessibility(
147147 ) = SwingUtilities .invokeLater {
148148 if (disposed) return @invokeLater
149149 val accessible = accessibleByNodeId(nodeId) ? : return @invokeLater
150- action(accessible, accessible.semanticsNode.config )
150+ action(accessible, accessible.semanticsConfig )
151151 }
152152
153153 /* *
@@ -187,88 +187,88 @@ internal class SemanticsOwnerAccessibility(
187187 */
188188 private fun onNodeChanged (
189189 accessible : ComposeAccessible ,
190- previousSemanticsNode : SemanticsNode ,
191- newSemanticsNode : SemanticsNode
190+ prevConfig : SemanticsConfiguration ,
192191 ) {
193192 val accessibleContext by lazy { accessible.composeAccessibleContext }
194- for (entry in newSemanticsNode.config) {
195- val prev = previousSemanticsNode.config.getOrNull(entry.key)
196- if (entry.value != prev) {
197- when (entry.key) {
198- SemanticsProperties .Text -> {
193+ for (entry in accessible.semanticsConfig) {
194+ val prevValue = prevConfig.getOrNull(entry.key)
195+ val newValue = entry.value
196+ if (newValue == prevValue) continue
197+
198+ when (entry.key) {
199+ SemanticsProperties .Text -> {
200+ accessibleContext.firePropertyChange(
201+ ACCESSIBLE_TEXT_PROPERTY ,
202+ prevValue, newValue
203+ )
204+ }
205+
206+ SemanticsProperties .EditableText -> {
207+ // The docs on ACCESSIBLE_TEXT_PROPERTY say that the value should be
208+ // an AccessibleTextSequence, but in reality, AccessibleJTextComponent
209+ // sends the position of the start of the change
210+ accessibleContext.firePropertyChange(
211+ ACCESSIBLE_TEXT_PROPERTY ,
212+ null ,
213+ 0 // Ideally, we should track the position of the change; 0 means everything changed
214+ )
215+ }
216+
217+ SemanticsProperties .TextSelectionRange -> {
218+ val prevTextSelectionRange = prevValue as ? TextRange
219+ val newTextSelectionRange = newValue as TextRange
220+
221+ val prevCaretPosition = prevTextSelectionRange?.end
222+ val newCaretPosition = newTextSelectionRange.end
223+ if (prevCaretPosition != newCaretPosition) {
199224 accessibleContext.firePropertyChange(
200- ACCESSIBLE_TEXT_PROPERTY ,
201- prev, entry.value
225+ ACCESSIBLE_CARET_PROPERTY ,
226+ prevCaretPosition, newCaretPosition
202227 )
203228 }
204229
205- SemanticsProperties .EditableText -> {
206- // The docs on ACCESSIBLE_TEXT_PROPERTY say that the value should be
207- // an AccessibleTextSequence, but in reality, AccessibleJTextComponent
208- // sends the position of the start of the change
230+ val text = accessible.semanticsConfig.getOrNull( SemanticsProperties .EditableText )
231+ val prevHasSelection = prevTextSelectionRange?.collapsed == false
232+ val nowHasSelection = ! newTextSelectionRange.collapsed
233+ if (prevHasSelection != nowHasSelection) {
209234 accessibleContext.firePropertyChange(
210- ACCESSIBLE_TEXT_PROPERTY ,
211- null ,
212- 0 // Ideally, we should track the position of the change; 0 means everything changed
235+ ACCESSIBLE_SELECTION_PROPERTY ,
236+ null , // AccessibleJTextComponent also sends oldValue = null
237+ text?.subSequence(newTextSelectionRange)
213238 )
214239 }
240+ }
215241
216- SemanticsProperties .TextSelectionRange -> {
217- val prevTextSelectionRange = prev as ? TextRange
218- val newTextSelectionRange = entry.value as TextRange
242+ SemanticsProperties .Focused ->
243+ if (newValue as Boolean ) {
244+ notifyOnFocusReceived(accessible)
245+ } else {
246+ notifyOnFocusLost(accessible)
247+ }
219248
220- val prevCaretPosition = prevTextSelectionRange?.end
221- val newCaretPosition = newTextSelectionRange.end
222- if (prevCaretPosition != newCaretPosition) {
249+ SemanticsProperties . ToggleableState -> {
250+ when (newValue as ToggleableState ) {
251+ ToggleableState . On ->
223252 accessibleContext.firePropertyChange(
224- ACCESSIBLE_CARET_PROPERTY ,
225- prevCaretPosition, newCaretPosition
253+ ACCESSIBLE_STATE_PROPERTY ,
254+ null , AccessibleState . CHECKED
226255 )
227- }
228256
229- val text = newSemanticsNode.config.getOrNull(SemanticsProperties .EditableText )
230- val prevHasSelection = prevTextSelectionRange?.collapsed == false
231- val nowHasSelection = ! newTextSelectionRange.collapsed
232- if (prevHasSelection != nowHasSelection) {
257+ ToggleableState .Off , ToggleableState .Indeterminate ->
233258 accessibleContext.firePropertyChange(
234- ACCESSIBLE_SELECTION_PROPERTY ,
235- null , // AccessibleJTextComponent also sends oldValue = null
236- text?.subSequence(newTextSelectionRange)
259+ ACCESSIBLE_STATE_PROPERTY ,
260+ AccessibleState .CHECKED , null
237261 )
238- }
239- }
240-
241- SemanticsProperties .Focused ->
242- if (entry.value as Boolean ) {
243- notifyOnFocusReceived(accessible)
244- } else {
245- notifyOnFocusLost(accessible)
246- }
247-
248- SemanticsProperties .ToggleableState -> {
249- when (entry.value as ToggleableState ) {
250- ToggleableState .On ->
251- accessibleContext.firePropertyChange(
252- ACCESSIBLE_STATE_PROPERTY ,
253- null , AccessibleState .CHECKED
254- )
255-
256- ToggleableState .Off , ToggleableState .Indeterminate ->
257- accessibleContext.firePropertyChange(
258- ACCESSIBLE_STATE_PROPERTY ,
259- AccessibleState .CHECKED , null
260- )
261- }
262262 }
263+ }
263264
264- SemanticsProperties .ProgressBarRangeInfo -> {
265- val value = entry.value as ProgressBarRangeInfo
266- accessibleContext.firePropertyChange(
267- ACCESSIBLE_VALUE_PROPERTY ,
268- (prev as ? ProgressBarRangeInfo )?.current,
269- value.current
270- )
271- }
265+ SemanticsProperties .ProgressBarRangeInfo -> {
266+ val value = newValue as ProgressBarRangeInfo
267+ accessibleContext.firePropertyChange(
268+ ACCESSIBLE_VALUE_PROPERTY ,
269+ (prevValue as ? ProgressBarRangeInfo )?.current,
270+ value.current
271+ )
272272 }
273273 }
274274 }
@@ -312,7 +312,7 @@ internal class SemanticsOwnerAccessibility(
312312 * An auxiliary mapping of semantics node ids to [ComposeAccessible]s that is swapped with
313313 * [accessibleByNodeId] on each sync, to avoid allocating memory on each sync.
314314 */
315- private var auxAccessibleByNodeId = mutableScatterMapOf< Int , ComposeAccessible >()
315+ private var auxAccessibleByNodeId = mutableIntObjectMapOf< ComposeAccessible >()
316316
317317 /* *
318318 * A list of callbacks ([onNodeAdded], [onNodeRemoved], [onNodeChanged]) to be made after
@@ -363,7 +363,9 @@ internal class SemanticsOwnerAccessibility(
363363 */
364364 private fun syncNodes () {
365365 fun SemanticsNode.isValid () = layoutNode.let { it.isPlaced && it.isAttached }
366- fun SemanticsNode.isInvisibleToA11y () = config.let {
366+ // `InvisibleToUser` and `HideFromAccessibility` are unmerged properties, so it's ok to get
367+ // them from `unmergedConfig`.
368+ fun SemanticsNode.isInvisibleToA11y () = unmergedConfig.let {
367369 @Suppress(" DEPRECATION" )
368370 it.contains(SemanticsProperties .InvisibleToUser ) ||
369371 it.contains(SemanticsProperties .HideFromAccessibility )
@@ -380,10 +382,10 @@ internal class SemanticsOwnerAccessibility(
380382
381383 val existingAccessible = previous[node.id]
382384 updated[node.id] = if (existingAccessible != null ) {
383- val prevSemanticsNode = existingAccessible.semanticsNode
385+ val prevSemanticsConfig = existingAccessible.semanticsConfig
384386 existingAccessible.semanticsNode = node
385387 delayedNodeNotifications.add {
386- onNodeChanged(existingAccessible, prevSemanticsNode, node )
388+ onNodeChanged(existingAccessible, prevSemanticsConfig )
387389 }
388390 existingAccessible
389391 }
@@ -454,7 +456,7 @@ internal class SemanticsOwnerAccessibility(
454456 private fun focusedAccessible (): ComposeAccessible ? {
455457 syncNodesIfInvalid()
456458 accessibleByNodeId.forEachValue { accessible ->
457- if (accessible.semanticsNode.config .getOrNull(SemanticsProperties .Focused ) == true ) {
459+ if (accessible.semanticsConfig .getOrNull(SemanticsProperties .Focused ) == true ) {
458460 return accessible
459461 }
460462 }
@@ -506,12 +508,13 @@ internal class SemanticsOwnerAccessibility(
506508 /* *
507509 * The set of "live" [SemanticsOwnerAccessibility]s.
508510 */
509- private val activeInstances = mutableSetOf<SemanticsOwnerAccessibility >()
511+ // Using a list instead of a set because set iterator is expensive (memory wise)
512+ private val activeInstances = mutableListOf<SemanticsOwnerAccessibility >()
510513
511514 /* *
512515 * The time of the latest accessibility call from the system.
513516 */
514- // Set initial value such that accessibilityRecentlyUsed is initially `false`
517+ // Set the initial value such that `recentlyUsed` is initially `false`
515518 private var lastUseTimeNanos: Long = System .nanoTime() - (MaxIdleTimeNanos + 1 )
516519
517520 /* *
0 commit comments