in shell/platform/android/io/flutter/view/AccessibilityBridge.java [1530:1764]
void updateSemantics(
@NonNull ByteBuffer buffer,
@NonNull String[] strings,
@NonNull ByteBuffer[] stringAttributeArgs) {
ArrayList<SemanticsNode> updated = new ArrayList<>();
while (buffer.hasRemaining()) {
int id = buffer.getInt();
SemanticsNode semanticsNode = getOrCreateSemanticsNode(id);
semanticsNode.updateWith(buffer, strings, stringAttributeArgs);
if (semanticsNode.hasFlag(Flag.IS_HIDDEN)) {
continue;
}
if (semanticsNode.hasFlag(Flag.IS_FOCUSED)) {
inputFocusedSemanticsNode = semanticsNode;
}
if (semanticsNode.hadPreviousConfig) {
updated.add(semanticsNode);
}
if (semanticsNode.platformViewId != -1
&& !platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
View embeddedView =
platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
if (embeddedView != null) {
embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
}
}
}
Set<SemanticsNode> visitedObjects = new HashSet<>();
SemanticsNode rootObject = getRootSemanticsNode();
List<SemanticsNode> newRoutes = new ArrayList<>();
if (rootObject != null) {
final float[] identity = new float[16];
Matrix.setIdentityM(identity, 0);
// In Android devices API 23 and above, the system nav bar can be placed on the left side
// of the screen in landscape mode. We must handle the translation ourselves for the
// a11y nodes.
if (Build.VERSION.SDK_INT >= 23) {
boolean needsToApplyLeftCutoutInset = true;
// In Android devices API 28 and above, the `layoutInDisplayCutoutMode` window attribute
// can be set to allow overlapping content within the cutout area. Query the attribute
// to figure out whether the content overlaps with the cutout and decide whether to
// apply cutout inset.
if (Build.VERSION.SDK_INT >= 28) {
needsToApplyLeftCutoutInset = doesLayoutInDisplayCutoutModeRequireLeftInset();
}
if (needsToApplyLeftCutoutInset) {
WindowInsets insets = rootAccessibilityView.getRootWindowInsets();
if (insets != null) {
if (!lastLeftFrameInset.equals(insets.getSystemWindowInsetLeft())) {
rootObject.globalGeometryDirty = true;
rootObject.inverseTransformDirty = true;
}
lastLeftFrameInset = insets.getSystemWindowInsetLeft();
Matrix.translateM(identity, 0, lastLeftFrameInset, 0, 0);
}
}
}
rootObject.updateRecursively(identity, visitedObjects, false);
rootObject.collectRoutes(newRoutes);
}
// Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the
// previously cached route id.
// Finds the last route that is not in the previous routes.
SemanticsNode lastAdded = null;
for (SemanticsNode semanticsNode : newRoutes) {
if (!flutterNavigationStack.contains(semanticsNode.id)) {
lastAdded = semanticsNode;
}
}
// If all the routes are in the previous route, get the last route.
if (lastAdded == null && newRoutes.size() > 0) {
lastAdded = newRoutes.get(newRoutes.size() - 1);
}
// There are two cases if lastAdded != nil
// 1. lastAdded is not in previous routes. In this case,
// lastAdded.id != previousRouteId
// 2. All new routes are in previous routes and
// lastAdded = newRoutes.last.
// In the first case, we need to announce new route. In the second case,
// we need to announce if one list is shorter than the other.
if (lastAdded != null
&& (lastAdded.id != previousRouteId || newRoutes.size() != flutterNavigationStack.size())) {
previousRouteId = lastAdded.id;
onWindowNameChange(lastAdded);
}
flutterNavigationStack.clear();
for (SemanticsNode semanticsNode : newRoutes) {
flutterNavigationStack.add(semanticsNode.id);
}
Iterator<Map.Entry<Integer, SemanticsNode>> it = flutterSemanticsTree.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, SemanticsNode> entry = it.next();
SemanticsNode object = entry.getValue();
if (!visitedObjects.contains(object)) {
willRemoveSemanticsNode(object);
it.remove();
}
}
// TODO(goderbauer): Send this event only once (!) for changed subtrees,
// see https://github.com/flutter/flutter/issues/14534
sendWindowContentChangeEvent(0);
for (SemanticsNode object : updated) {
if (object.didScroll()) {
AccessibilityEvent event =
obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
// Android doesn't support unbound scrolling. So we pretend there is a large
// bound (SCROLL_EXTENT_FOR_INFINITY), which you can never reach.
float position = object.scrollPosition;
float max = object.scrollExtentMax;
if (Float.isInfinite(object.scrollExtentMax)) {
max = SCROLL_EXTENT_FOR_INFINITY;
if (position > SCROLL_POSITION_CAP_FOR_INFINITY) {
position = SCROLL_POSITION_CAP_FOR_INFINITY;
}
}
if (Float.isInfinite(object.scrollExtentMin)) {
max += SCROLL_EXTENT_FOR_INFINITY;
if (position < -SCROLL_POSITION_CAP_FOR_INFINITY) {
position = -SCROLL_POSITION_CAP_FOR_INFINITY;
}
position += SCROLL_EXTENT_FOR_INFINITY;
} else {
max -= object.scrollExtentMin;
position -= object.scrollExtentMin;
}
if (object.hadAction(Action.SCROLL_UP) || object.hadAction(Action.SCROLL_DOWN)) {
event.setScrollY((int) position);
event.setMaxScrollY((int) max);
} else if (object.hadAction(Action.SCROLL_LEFT) || object.hadAction(Action.SCROLL_RIGHT)) {
event.setScrollX((int) position);
event.setMaxScrollX((int) max);
}
if (object.scrollChildren > 0) {
// We don't need to add 1 to the scroll index because TalkBack does this automagically.
event.setItemCount(object.scrollChildren);
event.setFromIndex(object.scrollIndex);
int visibleChildren = 0;
// handle hidden children at the beginning and end of the list.
for (SemanticsNode child : object.childrenInHitTestOrder) {
if (!child.hasFlag(Flag.IS_HIDDEN)) {
visibleChildren += 1;
}
}
if (BuildConfig.DEBUG) {
if (object.scrollIndex + visibleChildren > object.scrollChildren) {
Log.e(TAG, "Scroll index is out of bounds.");
}
if (object.childrenInHitTestOrder.isEmpty()) {
Log.e(TAG, "Had scrollChildren but no childrenInHitTestOrder");
}
}
// The setToIndex should be the index of the last visible child. Because we counted all
// children, including the first index we need to subtract one.
//
// [0, 1, 2, 3, 4, 5]
// ^ ^
// In the example above where 0 is the first visible index and 2 is the last, we will
// count 3 total visible children. We then subtract one to get the correct last visible
// index of 2.
event.setToIndex(object.scrollIndex + visibleChildren - 1);
}
sendAccessibilityEvent(event);
}
if (object.hasFlag(Flag.IS_LIVE_REGION) && object.didChangeLabel()) {
sendWindowContentChangeEvent(object.id);
}
if (accessibilityFocusedSemanticsNode != null
&& accessibilityFocusedSemanticsNode.id == object.id
&& !object.hadFlag(Flag.IS_SELECTED)
&& object.hasFlag(Flag.IS_SELECTED)) {
AccessibilityEvent event =
obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SELECTED);
event.getText().add(object.label);
sendAccessibilityEvent(event);
}
// If the object is the input-focused node, then tell the reader about it, but only if
// it has changed since the last update.
if (inputFocusedSemanticsNode != null
&& inputFocusedSemanticsNode.id == object.id
&& (lastInputFocusedSemanticsNode == null
|| lastInputFocusedSemanticsNode.id != inputFocusedSemanticsNode.id)) {
lastInputFocusedSemanticsNode = inputFocusedSemanticsNode;
sendAccessibilityEvent(
obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_FOCUSED));
} else if (inputFocusedSemanticsNode == null) {
// There's no TYPE_VIEW_CLEAR_FOCUSED event, so if the current input focus becomes
// null, then we just set the last one to null too, so that it sends the event again
// when something regains focus.
lastInputFocusedSemanticsNode = null;
}
if (inputFocusedSemanticsNode != null
&& inputFocusedSemanticsNode.id == object.id
&& object.hadFlag(Flag.IS_TEXT_FIELD)
&& object.hasFlag(Flag.IS_TEXT_FIELD)
// If we have a TextField that has InputFocus, we should avoid announcing it if something
// else we track has a11y focus. This needs to still work when, e.g., IME has a11y focus
// or the "PASTE" popup is used though.
// See more discussion at https://github.com/flutter/flutter/issues/23180
&& (accessibilityFocusedSemanticsNode == null
|| (accessibilityFocusedSemanticsNode.id == inputFocusedSemanticsNode.id))) {
String oldValue = object.previousValue != null ? object.previousValue : "";
String newValue = object.value != null ? object.value : "";
AccessibilityEvent event = createTextChangedEvent(object.id, oldValue, newValue);
if (event != null) {
sendAccessibilityEvent(event);
}
if (object.previousTextSelectionBase != object.textSelectionBase
|| object.previousTextSelectionExtent != object.textSelectionExtent) {
AccessibilityEvent selectionEvent =
obtainAccessibilityEvent(
object.id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
selectionEvent.getText().add(newValue);
selectionEvent.setFromIndex(object.textSelectionBase);
selectionEvent.setToIndex(object.textSelectionExtent);
selectionEvent.setItemCount(newValue.length());
sendAccessibilityEvent(selectionEvent);
}
}
}
}