Revert "Extract logic that decides whether or not a SemanticsNode (and its AccessibilityNodeInfo) will be `screenReaderFocusable`."
This reverts commit 34ddf05665b90a450198ccd032026c5adad9e944.
Reason for revert: behavior regression
Bug: 277758622
Change-Id: I730cfbbf2ffb286ef18d9fdb53bd8086be4ad46f
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 17391e0..ee20288 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -25,7 +25,6 @@
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
-import android.text.SpannableString
import android.util.Log
import android.view.MotionEvent
import android.view.View
@@ -724,17 +723,6 @@
}
}
- private fun isScreenReaderFocusable(
- node: SemanticsNode
- ): Boolean {
- val isSpeakingNode = node.infoContentDescriptionOrNull != null ||
- getInfoText(node) != null || getInfoStateDescriptionOrNull(node) != null ||
- getInfoIsCheckable(node)
-
- return node.unmergedConfig.isMergingSemanticsOfDescendants ||
- node.isUnmergedLeafNode && isSpeakingNode
- }
-
@VisibleForTesting
@OptIn(ExperimentalComposeUiApi::class)
fun populateAccessibilityNodeInfoProperties(
@@ -742,6 +730,15 @@
info: AccessibilityNodeInfoCompat,
semanticsNode: SemanticsNode
) {
+ val isUnmergedLeafNode =
+ !semanticsNode.isFake &&
+ semanticsNode.replacedChildren.isEmpty() &&
+ semanticsNode.layoutNode.findClosestParentNode {
+ it.outerSemantics
+ ?.collapsedSemanticsConfiguration()
+ ?.isMergingSemanticsOfDescendants == true
+ } == null
+
// set classname
info.className = ClassName
val role = semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.Role)
@@ -756,7 +753,7 @@
// Images are often minor children of larger widgets, so we only want to
// announce the Image role when the image itself is focusable.
if (role != Role.Image ||
- semanticsNode.isUnmergedLeafNode ||
+ isUnmergedLeafNode ||
semanticsNode.unmergedConfig.isMergingSemanticsOfDescendants
) {
info.className = className
@@ -801,17 +798,39 @@
setText(semanticsNode, info)
setContentInvalid(semanticsNode, info)
- setStateDescription(semanticsNode, info)
- setIsCheckable(semanticsNode, info)
+ info.stateDescription =
+ semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.StateDescription)
val toggleState = semanticsNode.unmergedConfig.getOrNull(
SemanticsProperties.ToggleableState
)
toggleState?.let {
- if (toggleState == ToggleableState.On) {
- info.isChecked = true
- } else if (toggleState == ToggleableState.Off) {
- info.isChecked = false
+ info.isCheckable = true
+ when (it) {
+ ToggleableState.On -> {
+ info.isChecked = true
+ // Unfortunately, talback has a bug of using "checked", so we set state
+ // description here
+ if (role == Role.Switch && info.stateDescription == null) {
+ info.stateDescription = view.context.resources.getString(R.string.on)
+ }
+ }
+
+ ToggleableState.Off -> {
+ info.isChecked = false
+ // Unfortunately, talkback has a bug of using "not checked", so we set state
+ // description here
+ if (role == Role.Switch && info.stateDescription == null) {
+ info.stateDescription = view.context.resources.getString(R.string.off)
+ }
+ }
+
+ ToggleableState.Indeterminate -> {
+ if (info.stateDescription == null) {
+ info.stateDescription =
+ view.context.resources.getString(R.string.indeterminate)
+ }
+ }
}
}
semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.Selected)?.let {
@@ -819,7 +838,18 @@
// Tab in native android uses selected property
info.isSelected = it
} else {
+ info.isCheckable = true
info.isChecked = it
+ if (info.stateDescription == null) {
+ // If a radio entry (radio button + text) is selectable, it won't have the role
+ // RadioButton, so if we use info.isCheckable/info.isChecked, talkback will say
+ // "checked/not checked" instead "selected/note selected".
+ info.stateDescription = if (it) {
+ view.context.resources.getString(R.string.selected)
+ } else {
+ view.context.resources.getString(R.string.not_selected)
+ }
+ }
}
}
@@ -1035,6 +1065,29 @@
rangeInfo.range.endInclusive,
rangeInfo.current
)
+ // let's set state description here and use state description change events.
+ // otherwise, we need to send out type_view_selected event, as the old android
+ // versions do. But the support for type_view_selected event for progress bars
+ // maybe deprecated in talkback in the future.
+ if (info.stateDescription == null) {
+ val valueRange = rangeInfo.range
+ val progress = (
+ if (valueRange.endInclusive - valueRange.start == 0f) 0f
+ else (rangeInfo.current - valueRange.start) /
+ (valueRange.endInclusive - valueRange.start)
+ ).coerceIn(0f, 1f)
+
+ // We only display 0% or 100% when it is exactly 0% or 100%.
+ val percent = when (progress) {
+ 0f -> 0
+ 1f -> 100
+ else -> (progress * 100).roundToInt().coerceIn(1, 99)
+ }
+ info.stateDescription =
+ view.context.resources.getString(R.string.template_percent, percent)
+ }
+ } else if (info.stateDescription == null) {
+ info.stateDescription = view.context.resources.getString(R.string.in_progress)
}
if (semanticsNode.unmergedConfig.contains(SemanticsActions.SetProgress) &&
semanticsNode.enabled()
@@ -1210,7 +1263,12 @@
}
}
- info.isScreenReaderFocusable = isScreenReaderFocusable(semanticsNode)
+ val isSpeakingNode = info.contentDescription != null || info.text != null ||
+ info.hintText != null || info.stateDescription != null || info.isCheckable
+
+ info.isScreenReaderFocusable =
+ semanticsNode.unmergedConfig.isMergingSemanticsOfDescendants ||
+ isUnmergedLeafNode && isSpeakingNode
if (idToBeforeMap[virtualViewId] != null) {
idToBeforeMap[virtualViewId]?.let { info.setTraversalBefore(view, it) }
@@ -1235,131 +1293,10 @@
}
}
- private fun getInfoStateDescriptionOrNull(
- node: SemanticsNode
- ): String? {
- var stateDescription = node.unmergedConfig.getOrNull(SemanticsProperties.StateDescription)
- val toggleState = node.unmergedConfig.getOrNull(SemanticsProperties.ToggleableState)
- val role = node.unmergedConfig.getOrNull(SemanticsProperties.Role)
-
- // Check toggle state and retrieve description accordingly
- toggleState?.let {
- when (it) {
- ToggleableState.On -> {
- // Unfortunately, talkback has a bug of using "checked", so we set state
- // description here
- if (role == Role.Switch && stateDescription == null) {
- stateDescription = view.context.resources.getString(R.string.on)
- }
- }
-
- ToggleableState.Off -> {
- // Unfortunately, talkback has a bug of using "not checked", so we set state
- // description here
- if (role == Role.Switch && stateDescription == null) {
- stateDescription = view.context.resources.getString(R.string.off)
- }
- }
-
- ToggleableState.Indeterminate -> {
- if (stateDescription == null) {
- stateDescription =
- view.context.resources.getString(R.string.indeterminate)
- }
- }
- }
- }
-
- // Check Selected property and retrieve description accordingly
- node.unmergedConfig.getOrNull(SemanticsProperties.Selected)?.let {
- if (role != Role.Tab) {
- if (stateDescription == null) {
- // If a radio entry (radio button + text) is selectable, it won't have the role
- // RadioButton, so if we use info.isCheckable/info.isChecked, talkback will say
- // "checked/not checked" instead "selected/note selected".
- stateDescription = if (it) {
- view.context.resources.getString(R.string.selected)
- } else {
- view.context.resources.getString(R.string.not_selected)
- }
- }
- }
- }
-
- // Check if a node has progress bar range info and retrieve description accordingly
- val rangeInfo =
- node.unmergedConfig.getOrNull(SemanticsProperties.ProgressBarRangeInfo)
- rangeInfo?.let {
- // let's set state description here and use state description change events.
- // otherwise, we need to send out type_view_selected event, as the old android
- // versions do. But the support for type_view_selected event for progress bars
- // maybe deprecated in talkback in the future.
- if (rangeInfo !== ProgressBarRangeInfo.Indeterminate) {
- if (stateDescription == null) {
- val valueRange = rangeInfo.range
- val progress = (
- if (valueRange.endInclusive - valueRange.start == 0f) 0f
- else (rangeInfo.current - valueRange.start) /
- (valueRange.endInclusive - valueRange.start)
- ).coerceIn(0f, 1f)
-
- // We only display 0% or 100% when it is exactly 0% or 100%.
- val percent = when (progress) {
- 0f -> 0
- 1f -> 100
- else -> (progress * 100).roundToInt().coerceIn(1, 99)
- }
- stateDescription =
- view.context.resources.getString(R.string.template_percent, percent)
- }
- } else if (stateDescription == null) {
- stateDescription = view.context.resources.getString(R.string.in_progress)
- }
- }
-
- return stateDescription
- }
-
- private fun setStateDescription(
+ private fun setText(
node: SemanticsNode,
info: AccessibilityNodeInfoCompat,
) {
- info.stateDescription = getInfoStateDescriptionOrNull(node)
- }
-
- private fun getInfoIsCheckable(
- node: SemanticsNode
- ): Boolean {
- var isCheckable = false
- val toggleState = node.unmergedConfig.getOrNull(SemanticsProperties.ToggleableState)
- val role = node.unmergedConfig.getOrNull(SemanticsProperties.Role)
-
- toggleState?.let {
- isCheckable = true
- }
-
- node.unmergedConfig.getOrNull(SemanticsProperties.Selected)?.let {
- if (role != Role.Tab) {
- isCheckable = true
- }
- }
-
- return isCheckable
- }
-
- private fun setIsCheckable(
- node: SemanticsNode,
- info: AccessibilityNodeInfoCompat
- ) {
- info.isCheckable = getInfoIsCheckable(node)
- }
-
- // This needs to be here instead of around line 3000 because we need access to the `view`
- // that is inside the `AndroidComposeViewAccessibilityDelegateCompat` class
- @OptIn(InternalTextApi::class)
- private fun getInfoText(
- node: SemanticsNode
- ): SpannableString? {
val fontFamilyResolver: FontFamily.Resolver = view.fontFamilyResolver
val editableTextToAssign = trimToSize(
node.unmergedConfig.getTextForTextField()
@@ -1380,14 +1317,8 @@
),
ParcelSafeTextLength
)
- return editableTextToAssign ?: textToAssign
- }
- private fun setText(
- node: SemanticsNode,
- info: AccessibilityNodeInfoCompat,
- ) {
- info.text = getInfoText(node)
+ info.text = editableTextToAssign ?: textToAssign
}
/**
@@ -2817,10 +2748,10 @@
}
if (bufferedContentCaptureDisappearedNodes.isNotEmpty()) {
session.notifyViewsDisappeared(
- bufferedContentCaptureDisappearedNodes
- .toList()
- .fastMap { it.toLong() }
- .toLongArray())
+ bufferedContentCaptureDisappearedNodes
+ .toList()
+ .fastMap { it.toLong() }
+ .toLongArray())
bufferedContentCaptureDisappearedNodes.clear()
}
}
@@ -3277,9 +3208,6 @@
return false
}
-private val SemanticsNode.infoContentDescriptionOrNull get() = this.unmergedConfig.getOrNull(
- SemanticsProperties.ContentDescription)?.firstOrNull()
-
@OptIn(ExperimentalComposeUiApi::class)
private fun SemanticsNode.excludeLineAndPageGranularities(): Boolean {
// text field that is not in focus
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index c56a5d2..ab6678e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -78,13 +78,6 @@
internal val unmergedConfig = outerSemanticsNode.collapsedSemanticsConfiguration()
- internal var isUnmergedLeafNode =
- !isFake && replacedChildren.isEmpty() && layoutNode.findClosestParentNode {
- it.outerSemantics
- ?.collapsedSemanticsConfiguration()
- ?.isMergingSemanticsOfDescendants == true
- } == null
-
/**
* The [LayoutInfo] that this is associated with.
*/