Skip to content

Commit 997b7c9

Browse files
jorge-cabfacebook-github-bot
authored andcommitted
Rewrite accessibilityOrder with virtual view hierarchy (#51692)
Summary: Pull Request resolved: #51692 The original algorithm for accessibilityOrder on Android had unexpected bugs. For some reason `.traversalAfter()` and `traversalBefore()` have unexpected behaviors when dealing with ancestor/descendant relationships getting more and more unexpected the further apart they are. So we are ditching that approach entirely. Now we have the view with accessibilityOrder create a virtual view hierarchy. We create a virtual node for each child that is in the order, and set the virtual node's position to be the same as the View it is trying to represent. We then also populate that node with the same stuff we populate regular ax nodes with the `populateAccessibilityNodeInfo()` function and the content description of the view it is backing so we get matching descriptions with what would otherwise be the normal announcement. **We have no way to exhaustively check every accessibility use case so we'll have to fine tune this as bugs come up, I'm expecting there to not be many issues since we populate the node the exact same way we populate every other node but anything that happens before React Native handles the node might miss some things** Changelog: [Internal] Reviewed By: joevilches Differential Revision: D74766296 fbshipit-source-id: 5e77c17bed1644bc5fbf5c1e19c3c6908cc1e3e9
1 parent c27a880 commit 997b7c9

4 files changed

Lines changed: 211 additions & 134 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,6 @@ public void setNativeId(@NonNull T view, @Nullable String nativeId) {
292292
if (view.getTag(R.id.accessibility_order_parent) != null) {
293293
ViewGroup accessibilityParent = (ViewGroup) view.getTag(R.id.accessibility_order_parent);
294294

295-
ReactAxOrderHelper.unsetAccessibilityOrder(accessibilityParent);
296295
accessibilityParent.setTag(R.id.accessibility_order_dirty, true);
297296

298297
accessibilityParent.notifySubtreeAccessibilityStateChanged(
@@ -334,12 +333,11 @@ public void onChildViewRemoved(View parent, View child) {
334333
view.setTag(R.id.accessibility_order_dirty, true);
335334
}
336335
});
337-
}
338336

339-
ReactAxOrderHelper.unsetAccessibilityOrder(view);
340-
((ViewGroup) view)
341-
.notifySubtreeAccessibilityStateChanged(
342-
view, view, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
337+
((ViewGroup) view)
338+
.notifySubtreeAccessibilityStateChanged(
339+
view, view, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
340+
}
343341
}
344342

345343
@ReactProp(name = ViewProps.ACCESSIBILITY_LABELLED_BY)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import android.view.ViewGroup;
1818
import android.view.accessibility.AccessibilityEvent;
1919
import android.widget.EditText;
20+
import android.widget.TextView;
2021
import androidx.annotation.NonNull;
2122
import androidx.annotation.Nullable;
2223
import androidx.core.view.ViewCompat;
@@ -41,8 +42,11 @@
4142
import com.facebook.react.uimanager.events.Event;
4243
import com.facebook.react.uimanager.events.EventDispatcher;
4344
import com.facebook.react.uimanager.util.ReactFindViewUtil;
45+
import java.util.ArrayList;
4446
import java.util.HashMap;
47+
import java.util.HashSet;
4548
import java.util.List;
49+
import java.util.Set;
4650

4751
/**
4852
* Utility class that handles the addition of a "role" for accessibility to either a View or
@@ -65,6 +69,7 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
6569
private static final String STATE_CHECKED = "checked";
6670

6771
private final View mView;
72+
private List<View> mAxOrderViews;
6873
private Handler mHandler;
6974
private final HashMap<Integer, String> mAccessibilityActionsMap;
7075

@@ -108,7 +113,6 @@ public static void setDelegate(
108113
if (!ViewCompat.hasAccessibilityDelegate(view)
109114
&& (view.getTag(R.id.accessibility_role) != null
110115
|| view.getTag(R.id.accessibility_order) != null
111-
|| view.getTag(R.id.accessibility_order_parent) != null
112116
|| view.getTag(R.id.accessibility_state) != null
113117
|| view.getTag(R.id.accessibility_actions) != null
114118
|| view.getTag(R.id.react_test_id) != null
@@ -134,22 +138,7 @@ protected View getHostView() {
134138
return mView;
135139
}
136140

137-
@Override
138-
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
139-
super.onInitializeAccessibilityNodeInfo(host, info);
140-
141-
if (host.getTag(R.id.accessibility_order_dirty) != null) {
142-
boolean isAxOrderDirty = (boolean) host.getTag(R.id.accessibility_order_dirty);
143-
if (isAxOrderDirty) {
144-
ReactAxOrderHelper.setCustomAccessibilityFocusOrder(host);
145-
host.setTag(R.id.accessibility_order_dirty, false);
146-
}
147-
}
148-
149-
if (host.getTag(R.id.accessibility_order_flow_to) != null) {
150-
ReactAxOrderHelper.applyFlowToTraversal(host, info);
151-
}
152-
141+
private void populateAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
153142
if (host.getTag(R.id.accessibility_state_expanded) != null) {
154143
final boolean accessibilityStateExpanded =
155144
(boolean) host.getTag(R.id.accessibility_state_expanded);
@@ -266,6 +255,34 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo
266255
}
267256
}
268257

258+
@Override
259+
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
260+
// If we set an accessibility order then all the focusing logic should go through our custom
261+
// virtual view tree hierarchy and ignore the default path
262+
ReadableArray axOrderIds = (ReadableArray) mView.getTag(R.id.accessibility_order);
263+
if (axOrderIds != null && axOrderIds.size() != 0) {
264+
265+
Boolean isAxOrderDirty = (Boolean) mView.getTag(R.id.accessibility_order_dirty);
266+
if (isAxOrderDirty != null && isAxOrderDirty) {
267+
List<String> axOrderIdsList = new ArrayList<>();
268+
Set<String> axOrderSet = new HashSet<>();
269+
for (int i = 0; i < axOrderIds.size(); i++) {
270+
String id = axOrderIds.getString(i);
271+
if (id != null) {
272+
axOrderIdsList.add(id);
273+
axOrderSet.add(id);
274+
}
275+
}
276+
277+
mAxOrderViews = ReactAxOrderHelper.processAxOrderTree(mView, axOrderIdsList, axOrderSet);
278+
}
279+
return;
280+
}
281+
282+
super.onInitializeAccessibilityNodeInfo(host, info);
283+
populateAccessibilityNodeInfo(host, info);
284+
}
285+
269286
@Override
270287
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
271288
super.onInitializeAccessibilityEvent(host, event);
@@ -436,17 +453,65 @@ public static void setRole(
436453

437454
@Override
438455
protected int getVirtualViewAt(float x, float y) {
439-
return INVALID_ID;
456+
if (mAxOrderViews == null) {
457+
return HOST_ID;
458+
}
459+
460+
int closestViewId = HOST_ID;
461+
int smallestArea = Integer.MAX_VALUE;
462+
463+
for (int i = 0; i < mAxOrderViews.size(); i++) {
464+
Rect bounds = ReactAxOrderHelper.getVirtualViewBounds(mView, mAxOrderViews.get(i));
465+
if (bounds.contains((int) x, (int) y)) {
466+
int area = bounds.width() * bounds.height();
467+
if (area < smallestArea) {
468+
smallestArea = area;
469+
closestViewId = i;
470+
}
471+
}
472+
}
473+
474+
return closestViewId;
440475
}
441476

442477
@Override
443-
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {}
478+
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
479+
if (mAxOrderViews != null && !mAxOrderViews.isEmpty()) {
480+
for (int i = 0; i < mAxOrderViews.size(); i++) {
481+
virtualViewIds.add(i);
482+
}
483+
}
484+
}
444485

445486
@Override
446487
protected void onPopulateNodeForVirtualView(
447488
int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
448-
node.setContentDescription("");
449-
node.setBoundsInParent(new Rect(0, 0, 1, 1));
489+
if (mView.getTag(R.id.accessibility_order) != null) {
490+
if (mAxOrderViews.size() <= virtualViewId) {
491+
node.setContentDescription("");
492+
node.setBoundsInParent(new Rect(0, 0, 1, 1));
493+
return;
494+
}
495+
496+
View virtualView = mAxOrderViews.get(virtualViewId);
497+
498+
node.setContentDescription("");
499+
if (virtualView == mView) {
500+
if (mView.getContentDescription() != null) {
501+
node.setContentDescription(mView.getContentDescription());
502+
}
503+
504+
if (mView instanceof TextView && ((TextView) mView).getText() != null) {
505+
node.setText(((TextView) mView).getText());
506+
}
507+
508+
populateAccessibilityNodeInfo(mView, node);
509+
node.setBoundsInParent(new Rect(0, 0, mView.getWidth(), mView.getHeight()));
510+
} else {
511+
node.setBoundsInParent(ReactAxOrderHelper.getVirtualViewBounds(mView, virtualView));
512+
}
513+
node.addChild(virtualView);
514+
}
450515
}
451516

452517
@Override
@@ -457,6 +522,10 @@ protected boolean onPerformActionForVirtualView(
457522

458523
@Override
459524
public @Nullable AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
525+
if (mView.getTag(R.id.accessibility_order) != null) {
526+
return super.getAccessibilityNodeProvider(host);
527+
}
528+
460529
return null;
461530
}
462531

0 commit comments

Comments
 (0)