Skip to content

Commit db17864

Browse files
authored
More accessibility optimizations (JetBrains#2906)
1 parent b774c9b commit db17864

2 files changed

Lines changed: 81 additions & 78 deletions

File tree

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/a11y/ComposeAccessible.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ internal class ComposeAccessible(
102102
/**
103103
* The (cached) [SemanticsNode.config] of [semanticsNode].
104104
*/
105-
private val semanticsConfig: SemanticsConfiguration
105+
val semanticsConfig: SemanticsConfiguration
106106
get() =
107107
cachedSemanticsConfig ?: semanticsNode.config.also {
108108
cachedSemanticsConfig = it
@@ -345,7 +345,7 @@ internal class ComposeAccessible(
345345
}
346346

347347
override fun getAccessibleChildrenCount(): Int {
348-
return semanticsNode.replacedChildren.size + auxiliaryChildren.size
348+
return traversalOrderedChildren.size + auxiliaryChildren.size
349349
}
350350

351351
override fun getAccessibleChild(index: Int): Accessible? {
@@ -983,4 +983,4 @@ private fun SemanticsNode.traversalOrderedChildren(
983983
return children.sortedBy {
984984
it.unmergedConfig.getOrNull(SemanticsProperties.TraversalIndex) ?: 0f
985985
}
986-
}
986+
}

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/a11y/SemanticsOwnerAccessibility.kt

Lines changed: 78 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package androidx.compose.ui.platform.a11y
1818

19-
import androidx.collection.mutableScatterMapOf
19+
import androidx.collection.mutableIntObjectMapOf
2020
import androidx.compose.ui.platform.PlatformComponent
2121
import androidx.compose.ui.semantics.ProgressBarRangeInfo
2222
import 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

Comments
 (0)