Repository navigation
Keep the Text accessibility delegate when the text has no links - #59521
hryhoriiK97 wants to merge 1 commit into
Conversation
Every Text update reset the accessibility delegate, creating a new ExploreByTouchHelper even when TalkBack is off and the text has no links. Keep the existing delegate when the text has no links before or after the update, and make an "auto" view important as setting the delegate does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Visual regressionNo visual differences at android: no baseline for this environment ios: no capture |
Device measurement — with and without this changeI'm carrying this change in an app on React Native 0.86.3 and measured it on two phones. Without it — each With it — the median is 8.9 µs on the S23 (−46%) and 1.6 µs on the RedMagic (−30%).
Release builds of the same app with and without this PR's three files, react-native 0.86.3 compiled from source, each run flinging a list that mounts rows of Every run
The runs alternated without, with, with, without on each phone. Two runs on the same side differ by at most 0.26 µs in median. QuerySELECT slice.dur FROM slice
JOIN thread_track ON slice.track_id = thread_track.id
WHERE thread_track.utid = <the app's main thread>
AND slice.name = 'PreparedLayoutTextViewManager.updateExtraData' |

Summary:
On Android, every Text update goes through
updateExtraDatain the Text view manager (PreparedLayoutTextViewManager, andReactTextViewManagerfor the older text view). There, the text's clickable spans are stored in a view tag and the accessibility delegate is reset, so that TalkBack can reach the links.Current behavior. The comment says this is for a text view that "contains any clickable spans", but
PreparedLayoutTextViewManagerchecksis Spanned, which is true for every Text, andReactTextViewManagerhas no check at all. So every update of every Text creates a newReactTextViewAccessibilityDelegate, even when TalkBack is off and the text has no links. Creating one is anExploreByTouchHelper: it looks up theAccessibilityManager, makes the view focusable and then restores it, and allocates aHandlerand a map. When the text has no links, nothing changes for the delegate, since it reads the links only when it's created.With this change. Both managers call a new
ReactTextViewAccessibilityDelegate.updateAccessibilityLinks. When the view already has aReactTextViewAccessibilityDelegateand the text has no links, before or after the update, it keeps that delegate. In every other case (the first update, links added, changed or removed, or another delegate on the view) it resets the delegate as before. A plain Text still gets the delegate on its first update;PreparedLayoutTextViewneeds it to expose its text to TalkBack.Setting a delegate through
ViewCompatalso makes a view whoseimportantForAccessibilityisautoimportant. The kept path does the same, so a Text set back toauto(by the prop, or by view recycling) still ends up important, as before. The one difference: the accessibility actions a delegate caches from theaccessibilityActionsprop now stay between text updates, as they already do for every other view, instead of being dropped with the old delegate.Changelog:
[ANDROID] [CHANGED] - Text updates keep the accessibility delegate when the text has no links
Test Plan:
--dry-run --set-exit-if-changed) passes on all changed files.ReactTextViewAccessibilityDelegateTest(4 Robolectric tests: the first update installs the text delegate; updates without links keep the same delegate; adding and removing links resets it; a view set back toautois important after an update). With the old code only "updates without links keep the delegate" fails.PreparedLayoutTextViewAccessibilityTextTestandReactTextViewPropertyTeststill pass.assembleRelease, arm64-v8a) and compiled withcmd package compile -m speed. The scenarios are a local Playground screen, not part of this PR: 200 numbers that change every 100 ms, and 60 lines of mixed-script text (emoji, CJK, Arabic, Hebrew, Polish) that change every 100 ms.adb shell setprop, not part of this PR) that runs either the old reset on every update or this change. Launches alternate A B B A A B B A, 4 per arm and scenario; 15 s Perfetto traces with the app's trace sections. The changes are medians.updateExtraDataper call, numbersupdateExtraDataper call, mixed scriptsThese times include the cost of the trace sections themselves, about half of
updateExtraData's samples in a sampled profile, so without tracing the relative gain is larger.uiautomator dumpofTextExample,AccessibilityExampleandAccessibilityAndroidExample, 4 scroll positions each, with the old and the new code: the same 166 nodes, with the same text, content description and focusable, clickable and important-for-accessibility flags (positions differ only by the scroll). I couldn't run TalkBack on the device I used.🤖 Generated with Claude Code