mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Remove Virtual View accessibilityOrder implementation (#52297)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/52297 Doing virtual views is the only way of making it possible to add the host view into the order. This however is too complex for very little gain, we are opting to go for a cleaner solution with the trade off of not being able to add the host view. Changelog: [Internal] Reviewed By: joevilches Differential Revision: D77278752 fbshipit-source-id: 709b995f51a9a03f6d07f2e24f8aea21d62d95c4
This commit is contained in:
committed by
Facebook GitHub Bot
parent
76ff1aa5c6
commit
5b245767d6
@@ -3659,7 +3659,6 @@ public class com/facebook/react/uimanager/ReactAccessibilityDelegate : androidx/
|
||||
public static final field TOP_ACCESSIBILITY_ACTION_EVENT Ljava/lang/String;
|
||||
public static final field sActionIdMap Ljava/util/HashMap;
|
||||
public fun <init> (Landroid/view/View;ZI)V
|
||||
public fun cleanUp ()V
|
||||
public static fun createNodeInfoFromView (Landroid/view/View;)Landroidx/core/view/accessibility/AccessibilityNodeInfoCompat;
|
||||
public fun getAccessibilityNodeProvider (Landroid/view/View;)Landroidx/core/view/accessibility/AccessibilityNodeProviderCompat;
|
||||
protected fun getHostView ()Landroid/view/View;
|
||||
|
||||
-7
@@ -19,7 +19,6 @@ import android.view.accessibility.AccessibilityEvent;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.AccessibilityDelegateCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.R;
|
||||
@@ -188,12 +187,6 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
|
||||
((BaseVMFocusChangeListener) focusChangeListener).detach(view);
|
||||
}
|
||||
|
||||
AccessibilityDelegateCompat axDelegate = ViewCompat.getAccessibilityDelegate(view);
|
||||
|
||||
if (axDelegate instanceof ReactAccessibilityDelegate) {
|
||||
((ReactAccessibilityDelegate) axDelegate).cleanUp();
|
||||
}
|
||||
|
||||
if (view instanceof ViewGroup) {
|
||||
((ViewGroup) view).setOnHierarchyChangeListener(null);
|
||||
}
|
||||
|
||||
+8
-122
@@ -16,7 +16,6 @@ import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.widget.EditText;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -42,11 +41,8 @@ import com.facebook.react.uimanager.common.ViewUtil;
|
||||
import com.facebook.react.uimanager.events.Event;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.uimanager.util.ReactFindViewUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Utility class that handles the addition of a "role" for accessibility to either a View or
|
||||
@@ -69,14 +65,9 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
|
||||
private static final String STATE_CHECKED = "checked";
|
||||
|
||||
private final View mView;
|
||||
private List<View> mAxOrderViews;
|
||||
private Handler mHandler;
|
||||
private final HashMap<Integer, String> mAccessibilityActionsMap;
|
||||
|
||||
@Nullable
|
||||
private AccessibilityManager.AccessibilityStateChangeListener accessibilityStateChangeListener =
|
||||
null;
|
||||
|
||||
@Nullable View mAccessibilityLabelledBy;
|
||||
|
||||
static {
|
||||
@@ -116,7 +107,6 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
|
||||
// problems, so leave it alone.
|
||||
if (!ViewCompat.hasAccessibilityDelegate(view)
|
||||
&& (view.getTag(R.id.accessibility_role) != null
|
||||
|| view.getTag(R.id.accessibility_order) != null
|
||||
|| view.getTag(R.id.accessibility_state) != null
|
||||
|| view.getTag(R.id.accessibility_actions) != null
|
||||
|| view.getTag(R.id.react_test_id) != null
|
||||
@@ -142,7 +132,10 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
|
||||
return mView;
|
||||
}
|
||||
|
||||
private void populateAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
|
||||
if (host.getTag(R.id.accessibility_state_expanded) != null) {
|
||||
final boolean accessibilityStateExpanded =
|
||||
(boolean) host.getTag(R.id.accessibility_state_expanded);
|
||||
@@ -259,52 +252,6 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
// If we set an accessibility order then all the focusing logic should go through our custom
|
||||
// virtual view tree hierarchy and ignore the default path
|
||||
ReadableArray axOrderIds = (ReadableArray) mView.getTag(R.id.accessibility_order);
|
||||
if (axOrderIds != null && axOrderIds.size() != 0) {
|
||||
info.setContentDescription("");
|
||||
info.setFocusable(false);
|
||||
|
||||
AccessibilityManager am =
|
||||
(AccessibilityManager) host.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||
|
||||
if (accessibilityStateChangeListener == null && am != null) {
|
||||
AccessibilityManager.AccessibilityStateChangeListener newAccessibilityStateChangeListener =
|
||||
enabled -> {
|
||||
if (!enabled) {
|
||||
ReactAxOrderHelper.restoreSubtreeFocusability(host);
|
||||
host.setTag(R.id.accessibility_order_dirty, true);
|
||||
}
|
||||
};
|
||||
|
||||
am.addAccessibilityStateChangeListener(newAccessibilityStateChangeListener);
|
||||
accessibilityStateChangeListener = newAccessibilityStateChangeListener;
|
||||
}
|
||||
|
||||
Boolean isAxOrderDirty = (Boolean) mView.getTag(R.id.accessibility_order_dirty);
|
||||
if (isAxOrderDirty != null && isAxOrderDirty) {
|
||||
List<String> axOrderIdsList = new ArrayList<>();
|
||||
Set<String> axOrderSet = new HashSet<>();
|
||||
for (int i = 0; i < axOrderIds.size(); i++) {
|
||||
String id = axOrderIds.getString(i);
|
||||
if (id != null) {
|
||||
axOrderIdsList.add(id);
|
||||
axOrderSet.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
mAxOrderViews = ReactAxOrderHelper.processAxOrderTree(mView, axOrderIdsList, axOrderSet);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
populateAccessibilityNodeInfo(host, info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(host, event);
|
||||
@@ -475,61 +422,17 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
|
||||
|
||||
@Override
|
||||
protected int getVirtualViewAt(float x, float y) {
|
||||
if (mAxOrderViews == null) {
|
||||
return HOST_ID;
|
||||
}
|
||||
|
||||
int closestViewId = HOST_ID;
|
||||
int smallestArea = Integer.MAX_VALUE;
|
||||
|
||||
for (int i = 0; i < mAxOrderViews.size(); i++) {
|
||||
Rect bounds = ReactAxOrderHelper.getVirtualViewBounds(mView, mAxOrderViews.get(i));
|
||||
if (bounds.contains((int) x, (int) y)) {
|
||||
int area = bounds.width() * bounds.height();
|
||||
if (area < smallestArea) {
|
||||
smallestArea = area;
|
||||
closestViewId = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return closestViewId;
|
||||
return INVALID_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
|
||||
if (mAxOrderViews != null && !mAxOrderViews.isEmpty()) {
|
||||
for (int i = 0; i < mAxOrderViews.size(); i++) {
|
||||
virtualViewIds.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {}
|
||||
|
||||
@Override
|
||||
protected void onPopulateNodeForVirtualView(
|
||||
int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
|
||||
if (mView.getTag(R.id.accessibility_order) != null) {
|
||||
if (mAxOrderViews.size() <= virtualViewId) {
|
||||
node.setContentDescription("");
|
||||
node.setBoundsInParent(new Rect(0, 0, 1, 1));
|
||||
return;
|
||||
}
|
||||
|
||||
View virtualView = mAxOrderViews.get(virtualViewId);
|
||||
|
||||
node.setContentDescription("");
|
||||
if (virtualView == mView) {
|
||||
if (mView.getContentDescription() != null) {
|
||||
node.setContentDescription(mView.getContentDescription());
|
||||
}
|
||||
|
||||
populateAccessibilityNodeInfo(mView, node);
|
||||
node.setBoundsInParent(new Rect(0, 0, mView.getWidth(), mView.getHeight()));
|
||||
} else {
|
||||
node.setBoundsInParent(ReactAxOrderHelper.getVirtualViewBounds(mView, virtualView));
|
||||
}
|
||||
node.addChild(virtualView);
|
||||
}
|
||||
node.setContentDescription("");
|
||||
node.setBoundsInParent(new Rect(0, 0, 1, 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -540,10 +443,6 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
|
||||
|
||||
@Override
|
||||
public @Nullable AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
|
||||
if (mView.getTag(R.id.accessibility_order) != null) {
|
||||
return super.getAccessibilityNodeProvider(host);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1121,17 +1020,4 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In case a view with accessibilityOrder is unmounted we need a way to clean up the listener on
|
||||
// this delegate
|
||||
public void cleanUp() {
|
||||
if (accessibilityStateChangeListener != null) {
|
||||
AccessibilityManager am =
|
||||
(AccessibilityManager) mView.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||
if (am != null) {
|
||||
am.removeAccessibilityStateChangeListener(accessibilityStateChangeListener);
|
||||
}
|
||||
accessibilityStateChangeListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-181
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.uimanager
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.facebook.react.R
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
|
||||
private object ReactAxOrderHelper {
|
||||
/**
|
||||
* Processes the View tree that begins at the View with AccessibilityOrder set
|
||||
*
|
||||
* Disables accessibility for views not included in the specified accessibility order.
|
||||
*
|
||||
* This method emulates iOS's focusing order behavior to facilitate cross-platform code sharing.
|
||||
* It disables accessibility for views that are either not part of the accessibility order or
|
||||
* don't have a container that belongs to the accessibility order.
|
||||
*
|
||||
* The container/element concept is borrowed from iOS, where a "container" is a non-accessible
|
||||
* view with children, and an "element" is any accessible view.
|
||||
*
|
||||
* @return an array of views following the accessibility order
|
||||
*/
|
||||
@JvmStatic
|
||||
fun processAxOrderTree(
|
||||
root: View,
|
||||
axOrderIds: MutableList<String?>,
|
||||
axOrderSet: MutableSet<String?>
|
||||
): List<View> {
|
||||
val axOrderViews = Array(axOrderIds.size) { mutableListOf<View?>() }.toMutableList()
|
||||
|
||||
fun traverseAndBuildAxOrder(parent: View, view: View, containerId: String?) {
|
||||
val nativeId = view.getTag(R.id.view_tag_native_id) as String?
|
||||
|
||||
val isContained = (containerId != null && axOrderSet.contains(containerId))
|
||||
val isIncluded = (nativeId != null && axOrderSet.contains(nativeId))
|
||||
|
||||
val isNestedAxOrder = view.getTag(R.id.accessibility_order) != null && view != parent
|
||||
|
||||
if (isIncluded && view.isFocusable) {
|
||||
axOrderViews[axOrderIds.indexOf(nativeId)].add(view)
|
||||
if (parent != view) {
|
||||
view.setTag(R.id.accessibility_order_parent, parent)
|
||||
}
|
||||
} else if (isContained && view.isFocusable) {
|
||||
axOrderViews[axOrderIds.indexOf(containerId)].add(view)
|
||||
if (parent != view) {
|
||||
view.setTag(R.id.accessibility_order_parent, parent)
|
||||
}
|
||||
}
|
||||
|
||||
if (isNestedAxOrder) {
|
||||
val nestedOrder = view.getTag(R.id.accessibility_order) as ReadableArray
|
||||
for (i in 0 until nestedOrder.size()) {
|
||||
val id = nestedOrder.getString(i)
|
||||
if (id != null) {
|
||||
val insertIdx = axOrderIds.indexOf(nativeId) + 1
|
||||
if (insertIdx < axOrderIds.size) {
|
||||
axOrderIds.add(axOrderIds.indexOf(nativeId) + 1 + i, id)
|
||||
axOrderViews.add(axOrderIds.indexOf(nativeId) + 1 + i, mutableListOf())
|
||||
} else {
|
||||
axOrderIds.add(id)
|
||||
axOrderViews.add(mutableListOf())
|
||||
}
|
||||
|
||||
axOrderSet.add(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't traverse the children of a nested accessibility order
|
||||
if (view is ViewGroup) {
|
||||
val axChildren: ArrayList<View> = getAxChildren(view)
|
||||
|
||||
// If the View is a "container" (Not focusable but is included in the order) We add all its
|
||||
// children to the order.
|
||||
if (containerId != null) {
|
||||
for (i in 0 until axChildren.size) {
|
||||
traverseAndBuildAxOrder(parent, axChildren[i], containerId)
|
||||
}
|
||||
} else if (!view.isFocusable && isIncluded) {
|
||||
for (i in 0 until axChildren.size) {
|
||||
traverseAndBuildAxOrder(parent, axChildren[i], nativeId)
|
||||
}
|
||||
} else {
|
||||
for (i in 0 until axChildren.size) {
|
||||
traverseAndBuildAxOrder(parent, axChildren[i], null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isIncluded && !isContained && parent != view) {
|
||||
if (view.getTag(R.id.original_focusability) == null) {
|
||||
view.setTag(R.id.original_focusability, view.isFocusable)
|
||||
}
|
||||
view.isFocusable = false
|
||||
}
|
||||
}
|
||||
|
||||
traverseAndBuildAxOrder(
|
||||
root,
|
||||
root,
|
||||
null,
|
||||
)
|
||||
|
||||
val result = mutableListOf<View>()
|
||||
for (viewList in axOrderViews) {
|
||||
for (view in viewList) {
|
||||
if (view != null) {
|
||||
result.add(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.setTag(R.id.accessibility_order_dirty, false)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getVirtualViewBounds(host: View, virtualView: View): Rect {
|
||||
var currentView: View = virtualView
|
||||
val viewBoundsInParent =
|
||||
Rect(virtualView.left, virtualView.top, virtualView.right, virtualView.bottom)
|
||||
while (currentView.parent != host && currentView != host) {
|
||||
val parent = currentView.parent as View
|
||||
viewBoundsInParent.top += parent.top
|
||||
viewBoundsInParent.bottom += parent.top
|
||||
viewBoundsInParent.left += parent.left
|
||||
viewBoundsInParent.right += parent.left
|
||||
currentView = parent
|
||||
}
|
||||
|
||||
return viewBoundsInParent
|
||||
}
|
||||
|
||||
private fun getAxChildren(host: ViewGroup): ArrayList<View> {
|
||||
val axChildren: ArrayList<View> = ArrayList()
|
||||
|
||||
// When host has an accessibilityNodeProvider it means the order is not default so ViewGroup's
|
||||
// method bails, in this case we should just add the children to the node in the edge case where
|
||||
// we need to coopt at accessibilityOrderParent level
|
||||
if (host.accessibilityNodeProvider != null) {
|
||||
for (i in 0 until host.childCount) {
|
||||
val child = host.getChildAt(i)
|
||||
if (child != null) {
|
||||
axChildren.add(child)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This extracts the children of host sorted in accessibility order, this is by layout
|
||||
// top to bottom, left to right
|
||||
host.addChildrenForAccessibility(axChildren)
|
||||
}
|
||||
return axChildren
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun restoreSubtreeFocusability(view: View) {
|
||||
val originalFocusability = view.getTag(R.id.original_focusability)
|
||||
if (originalFocusability is Boolean) {
|
||||
view.isFocusable = originalFocusability
|
||||
}
|
||||
|
||||
if (view is ViewGroup) {
|
||||
for (i in 0 until view.childCount) {
|
||||
val child = view.getChildAt(i)
|
||||
if (child != null) {
|
||||
restoreSubtreeFocusability(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user