mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Delegate a11y events and action to/from embedded Android platform views. (flutter/engine#8250)
Delegate a11y events and action to/from embedded Android platfrom views. This handles delegation of: * AccessibilityNodeProvider#performAction * ViewGroup#requestSendAccessibilityEvent * View#onHoverEvent Additionally updates the currently input accessibility focused node state that is tracked by the a11y bridge when an embedded view's node is focused.
This commit is contained in:
parent
0d3dd80099
commit
968cefaef1
@ -511,6 +511,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StandardM
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StringCodec.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformView.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java
|
||||
|
||||
@ -153,6 +153,7 @@ java_library("flutter_shell_java") {
|
||||
"io/flutter/plugin/common/StringCodec.java",
|
||||
"io/flutter/plugin/editing/InputConnectionAdaptor.java",
|
||||
"io/flutter/plugin/editing/TextInputPlugin.java",
|
||||
"io/flutter/plugin/platform/AccessibilityEventsDelegate.java",
|
||||
"io/flutter/plugin/platform/PlatformPlugin.java",
|
||||
"io/flutter/plugin/platform/PlatformView.java",
|
||||
"io/flutter/plugin/platform/PlatformViewFactory.java",
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package io.flutter.plugin.platform;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import io.flutter.view.AccessibilityBridge;
|
||||
|
||||
/**
|
||||
* Delegates accessibility events to the currently attached accessibility bridge if one is attached.
|
||||
*/
|
||||
class AccessibilityEventsDelegate {
|
||||
private AccessibilityBridge accessibilityBridge;
|
||||
|
||||
/**
|
||||
* Delegates handling of {@link android.view.ViewParent#requestSendAccessibilityEvent} to the accessibility bridge.
|
||||
*
|
||||
* This is a no-op if there is no accessibility delegate set.
|
||||
*
|
||||
* This is used by embedded platform views to propagate accessibility events from their view hierarchy to the
|
||||
* accessibility bridge.
|
||||
*
|
||||
* As the embedded view doesn't have to be the only View in the embedded hierarchy (it can have child views) and the
|
||||
* event might have been originated from any view in this hierarchy, this method gets both a reference to the
|
||||
* embedded platform view, and a reference to the view from its hierarchy that sent the event.
|
||||
*
|
||||
* @param embeddedView the embedded platform view for which the event is delegated
|
||||
* @param eventOrigin the view in the embedded view's hierarchy that sent the event.
|
||||
* @return True if the event was sent.
|
||||
*/
|
||||
public boolean requestSendAccessibilityEvent(@NonNull View embeddedView, @NonNull View eventOrigin, @NonNull AccessibilityEvent event) {
|
||||
if (accessibilityBridge == null) {
|
||||
return false;
|
||||
}
|
||||
return accessibilityBridge.externalViewRequestSendAccessibilityEvent(embeddedView, eventOrigin, event);
|
||||
}
|
||||
|
||||
/*
|
||||
* This setter should only be used directly in PlatformViewsController when attached/detached to an accessibility
|
||||
* bridge.
|
||||
*/
|
||||
void setAccessibilityBridge(@Nullable AccessibilityBridge accessibilityBridge) {
|
||||
this.accessibilityBridge = accessibilityBridge;
|
||||
}
|
||||
}
|
||||
@ -52,13 +52,14 @@ public class PlatformViewsController implements MethodChannel.MethodCallHandler,
|
||||
private BinaryMessenger mMessenger;
|
||||
|
||||
// The accessibility bridge to which accessibility events form the platform views will be dispatched.
|
||||
private AccessibilityBridge accessibilityBridge;
|
||||
private final AccessibilityEventsDelegate mAccessibilityEventsDelegate;
|
||||
|
||||
private final HashMap<Integer, VirtualDisplayController> vdControllers;
|
||||
|
||||
public PlatformViewsController() {
|
||||
mRegistry = new PlatformViewRegistryImpl();
|
||||
vdControllers = new HashMap<>();
|
||||
mAccessibilityEventsDelegate = new AccessibilityEventsDelegate();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,12 +101,12 @@ public class PlatformViewsController implements MethodChannel.MethodCallHandler,
|
||||
|
||||
@Override
|
||||
public void attachAccessibilityBridge(AccessibilityBridge accessibilityBridge) {
|
||||
this.accessibilityBridge = accessibilityBridge;
|
||||
mAccessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detachAccessibiltyBridge() {
|
||||
this.accessibilityBridge = null;
|
||||
mAccessibilityEventsDelegate.setAccessibilityBridge(null);
|
||||
}
|
||||
|
||||
public PlatformViewRegistry getRegistry() {
|
||||
@ -201,6 +202,7 @@ public class PlatformViewsController implements MethodChannel.MethodCallHandler,
|
||||
TextureRegistry.SurfaceTextureEntry textureEntry = mTextureRegistry.createSurfaceTexture();
|
||||
VirtualDisplayController vdController = VirtualDisplayController.create(
|
||||
mContext,
|
||||
mAccessibilityEventsDelegate,
|
||||
viewFactory,
|
||||
textureEntry,
|
||||
toPhysicalPixels(logicalWidth),
|
||||
|
||||
@ -13,6 +13,7 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import java.lang.reflect.*;
|
||||
@ -57,6 +58,9 @@ class SingleViewPresentation extends Presentation {
|
||||
|
||||
private final PlatformViewFactory mViewFactory;
|
||||
|
||||
// A reference to the current accessibility bridge to which accessibility events will be delegated.
|
||||
private final AccessibilityEventsDelegate mAccessibilityEventsDelegate;
|
||||
|
||||
// This is the view id assigned by the Flutter framework to the embedded view, we keep it here
|
||||
// so when we create the platform view we can tell it its view id.
|
||||
private int mViewId;
|
||||
@ -67,7 +71,7 @@ class SingleViewPresentation extends Presentation {
|
||||
|
||||
// The root view for the presentation, it has 2 childs: mContainer which contains the embedded view, and
|
||||
// mFakeWindowRootView which contains views that were added directly to the presentation's window manager.
|
||||
private FrameLayout mRootView;
|
||||
private AccessibilityDelegatingFrameLayout mRootView;
|
||||
|
||||
// Contains the embedded platform view (mView.getView()) when it is attached to the presentation.
|
||||
private FrameLayout mContainer;
|
||||
@ -82,10 +86,13 @@ class SingleViewPresentation extends Presentation {
|
||||
Context outerContext,
|
||||
Display display,
|
||||
PlatformViewFactory viewFactory,
|
||||
AccessibilityEventsDelegate accessibilityEventsDelegate,
|
||||
int viewId,
|
||||
Object createParams) {
|
||||
Object createParams
|
||||
) {
|
||||
super(outerContext, display);
|
||||
mViewFactory = viewFactory;
|
||||
mAccessibilityEventsDelegate = accessibilityEventsDelegate;
|
||||
mViewId = viewId;
|
||||
mCreateParams = createParams;
|
||||
mState = new PresentationState();
|
||||
@ -102,8 +109,14 @@ class SingleViewPresentation extends Presentation {
|
||||
* <p>The display's density must match the density of the context used
|
||||
* when the view was created.
|
||||
*/
|
||||
public SingleViewPresentation(Context outerContext, Display display, PresentationState state) {
|
||||
public SingleViewPresentation(
|
||||
Context outerContext,
|
||||
Display display,
|
||||
AccessibilityEventsDelegate accessibilityEventsDelegate,
|
||||
PresentationState state
|
||||
) {
|
||||
super(outerContext, display);
|
||||
mAccessibilityEventsDelegate = accessibilityEventsDelegate;
|
||||
mViewFactory = null;
|
||||
mState = state;
|
||||
getWindow().setFlags(
|
||||
@ -130,8 +143,9 @@ class SingleViewPresentation extends Presentation {
|
||||
mState.mView = mViewFactory.create(context, mViewId, mCreateParams);
|
||||
}
|
||||
|
||||
mContainer.addView(mState.mView.getView());
|
||||
mRootView = new FrameLayout(getContext());
|
||||
View embeddedView = mState.mView.getView();
|
||||
mContainer.addView(embeddedView);
|
||||
mRootView = new AccessibilityDelegatingFrameLayout(getContext(), mAccessibilityEventsDelegate, embeddedView);
|
||||
mRootView.addView(mContainer);
|
||||
mRootView.addView(mState.mFakeWindowRootView);
|
||||
setContentView(mRootView);
|
||||
@ -320,4 +334,24 @@ class SingleViewPresentation extends Presentation {
|
||||
mFakeWindowRootView.updateViewLayout(view, layoutParams);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AccessibilityDelegatingFrameLayout extends FrameLayout {
|
||||
private final AccessibilityEventsDelegate mAccessibilityEventsDelegate;
|
||||
private final View mEmbeddedView;
|
||||
|
||||
public AccessibilityDelegatingFrameLayout(
|
||||
Context context,
|
||||
AccessibilityEventsDelegate accessibilityEventsDelegate,
|
||||
View ebeddedView
|
||||
) {
|
||||
super(context);
|
||||
mAccessibilityEventsDelegate = accessibilityEventsDelegate;
|
||||
mEmbeddedView = ebeddedView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
|
||||
return mAccessibilityEventsDelegate.requestSendAccessibilityEvent(mEmbeddedView, child, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ class VirtualDisplayController {
|
||||
|
||||
public static VirtualDisplayController create(
|
||||
Context context,
|
||||
AccessibilityEventsDelegate accessibilityEventsDelegate,
|
||||
PlatformViewFactory viewFactory,
|
||||
TextureRegistry.SurfaceTextureEntry textureEntry,
|
||||
int width,
|
||||
@ -45,10 +46,11 @@ class VirtualDisplayController {
|
||||
}
|
||||
|
||||
return new VirtualDisplayController(
|
||||
context, virtualDisplay, viewFactory, surface, textureEntry, viewId, createParams);
|
||||
context, accessibilityEventsDelegate, virtualDisplay, viewFactory, surface, textureEntry, viewId, createParams);
|
||||
}
|
||||
|
||||
private final Context mContext;
|
||||
private final AccessibilityEventsDelegate mAccessibilityEventsDelegate;
|
||||
private final int mDensityDpi;
|
||||
private final TextureRegistry.SurfaceTextureEntry mTextureEntry;
|
||||
private VirtualDisplay mVirtualDisplay;
|
||||
@ -58,6 +60,7 @@ class VirtualDisplayController {
|
||||
|
||||
private VirtualDisplayController(
|
||||
Context context,
|
||||
AccessibilityEventsDelegate accessibilityEventsDelegate,
|
||||
VirtualDisplay virtualDisplay,
|
||||
PlatformViewFactory viewFactory,
|
||||
Surface surface,
|
||||
@ -65,13 +68,14 @@ class VirtualDisplayController {
|
||||
int viewId,
|
||||
Object createParams
|
||||
) {
|
||||
mContext = context;
|
||||
mAccessibilityEventsDelegate = accessibilityEventsDelegate;
|
||||
mTextureEntry = textureEntry;
|
||||
mSurface = surface;
|
||||
mContext = context;
|
||||
mVirtualDisplay = virtualDisplay;
|
||||
mDensityDpi = context.getResources().getDisplayMetrics().densityDpi;
|
||||
mPresentation = new SingleViewPresentation(
|
||||
context, mVirtualDisplay.getDisplay(), viewFactory, viewId, createParams);
|
||||
context, mVirtualDisplay.getDisplay(), viewFactory, accessibilityEventsDelegate, viewId, createParams);
|
||||
mPresentation.show();
|
||||
}
|
||||
|
||||
@ -121,7 +125,7 @@ class VirtualDisplayController {
|
||||
public void onViewDetachedFromWindow(View v) {}
|
||||
});
|
||||
|
||||
mPresentation = new SingleViewPresentation(mContext, mVirtualDisplay.getDisplay(), presentationState);
|
||||
mPresentation = new SingleViewPresentation(mContext, mVirtualDisplay.getDisplay(), mAccessibilityEventsDelegate, presentationState);
|
||||
mPresentation.show();
|
||||
}
|
||||
|
||||
|
||||
@ -5,9 +5,7 @@
|
||||
package io.flutter.view;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
@ -23,7 +21,6 @@ import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
@ -158,9 +155,23 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
|
||||
// The {@code SemanticsNode} within Flutter that currently has the focus of Android's
|
||||
// accessibility system.
|
||||
//
|
||||
// This is null when a node embedded by the AccessibilityViewEmbedder has the focus.
|
||||
@Nullable
|
||||
private SemanticsNode accessibilityFocusedSemanticsNode;
|
||||
|
||||
// The virtual ID of the currently embedded node with accessibility focus.
|
||||
//
|
||||
// This is the ID of a node generated by the AccessibilityViewEmbedder if an embedded node is focused,
|
||||
// null otherwise.
|
||||
private Integer embeddedAccessibilityFocusedNodeId;
|
||||
|
||||
// The virtual ID of the currently embedded node with input focus.
|
||||
//
|
||||
// This is the ID of a node generated by the AccessibilityViewEmbedder if an embedded node is focused,
|
||||
// null otherwise.
|
||||
private Integer embeddedInputFocusedNodeId;
|
||||
|
||||
// The accessibility features that should currently be active within Flutter, represented as
|
||||
// a bitmask whose values comes from {@link AccessibilityFeature}.
|
||||
private int accessibilityFeatureFlags = 0;
|
||||
@ -762,6 +773,14 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
*/
|
||||
@Override
|
||||
public boolean performAction(int virtualViewId, int accessibilityAction, @Nullable Bundle arguments) {
|
||||
if (virtualViewId >= MIN_ENGINE_GENERATED_NODE_ID) {
|
||||
// The node is in the engine generated range, and is handled by the accessibility view embedder.
|
||||
boolean didPerform = accessibilityViewEmbedder.performAction(virtualViewId, accessibilityAction, arguments);
|
||||
if (didPerform && accessibilityAction == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
|
||||
embeddedAccessibilityFocusedNodeId = null;
|
||||
}
|
||||
return didPerform;
|
||||
}
|
||||
SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId);
|
||||
if (semanticsNode == null) {
|
||||
return false;
|
||||
@ -841,6 +860,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
|
||||
);
|
||||
accessibilityFocusedSemanticsNode = null;
|
||||
embeddedAccessibilityFocusedNodeId = null;
|
||||
return true;
|
||||
}
|
||||
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
|
||||
@ -1014,12 +1034,18 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
if (inputFocusedSemanticsNode != null) {
|
||||
return createAccessibilityNodeInfo(inputFocusedSemanticsNode.id);
|
||||
}
|
||||
if (embeddedInputFocusedNodeId != null) {
|
||||
return createAccessibilityNodeInfo(embeddedInputFocusedNodeId);
|
||||
}
|
||||
}
|
||||
// Fall through to check FOCUS_ACCESSIBILITY
|
||||
case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: {
|
||||
if (accessibilityFocusedSemanticsNode != null) {
|
||||
return createAccessibilityNodeInfo(accessibilityFocusedSemanticsNode.id);
|
||||
}
|
||||
if (embeddedAccessibilityFocusedNodeId != null) {
|
||||
return createAccessibilityNodeInfo(embeddedAccessibilityFocusedNodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@ -1090,6 +1116,11 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
SemanticsNode semanticsNodeUnderCursor = getRootSemanticsNode().hitTest(new float[] {event.getX(), event.getY(), 0, 1});
|
||||
if (semanticsNodeUnderCursor.platformViewId != -1) {
|
||||
return accessibilityViewEmbedder.onAccessibilityHoverEvent(semanticsNodeUnderCursor.id, event);
|
||||
}
|
||||
|
||||
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER || event.getAction() == MotionEvent.ACTION_HOVER_MOVE) {
|
||||
handleTouchExploration(event.getX(), event.getY());
|
||||
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||
@ -2061,4 +2092,46 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
return sb.length() > 0 ? sb.toString() : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates handling of {@link android.view.ViewParent#requestSendAccessibilityEvent} to the accessibility bridge.
|
||||
*
|
||||
* This is used by embedded platform views to propagate accessibility events from their view hierarchy to the
|
||||
* accessibility bridge.
|
||||
*
|
||||
* As the embedded view doesn't have to be the only View in the embedded hierarchy (it can have child views) and the
|
||||
* event might have been originated from any view in this hierarchy, this method gets both a reference to the
|
||||
* embedded platform view, and a reference to the view from its hierarchy that sent the event.
|
||||
*
|
||||
* @param embeddedView the embedded platform view for which the event is delegated
|
||||
* @param eventOrigin the view in the embedded view's hierarchy that sent the event.
|
||||
* @return True if the event was sent.
|
||||
*/
|
||||
public boolean externalViewRequestSendAccessibilityEvent(View embeddedView, View eventOrigin, AccessibilityEvent event) {
|
||||
if (!accessibilityViewEmbedder.requestSendAccessibilityEvent(embeddedView, eventOrigin, event)){
|
||||
return false;
|
||||
}
|
||||
Integer virtualNodeId = accessibilityViewEmbedder.getRecordFlutterId(embeddedView, event);
|
||||
if (virtualNodeId == null) {
|
||||
return false;
|
||||
}
|
||||
switch(event.getEventType()) {
|
||||
case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
|
||||
hoveredObject = null;
|
||||
break;
|
||||
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
|
||||
embeddedAccessibilityFocusedNodeId = virtualNodeId;
|
||||
accessibilityFocusedSemanticsNode = null;
|
||||
break;
|
||||
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
|
||||
embeddedInputFocusedNodeId = null;
|
||||
embeddedAccessibilityFocusedNodeId = null;
|
||||
break;
|
||||
case AccessibilityEvent.TYPE_VIEW_FOCUSED:
|
||||
embeddedInputFocusedNodeId = virtualNodeId;
|
||||
inputFocusedSemanticsNode = null;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,11 +6,17 @@ package io.flutter.view;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.accessibility.AccessibilityNodeProvider;
|
||||
import android.view.accessibility.AccessibilityRecord;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
@ -53,7 +59,7 @@ class AccessibilityViewEmbedder {
|
||||
|
||||
private int nextFlutterId;
|
||||
|
||||
AccessibilityViewEmbedder(View rootAccessibiiltyView, int firstVirtualNodeId) {
|
||||
AccessibilityViewEmbedder(@NonNull View rootAccessibiiltyView, int firstVirtualNodeId) {
|
||||
reflectionAccessors = new ReflectionAccessors();
|
||||
flutterIdToOrigin = new SparseArray<>();
|
||||
this.rootAccessibilityView = rootAccessibiiltyView;
|
||||
@ -68,24 +74,23 @@ class AccessibilityViewEmbedder {
|
||||
* @param flutterId the virtual accessibility ID for the node in flutter accessibility tree
|
||||
* @param displayBounds the display bounds for the node in screen coordinates
|
||||
*/
|
||||
public AccessibilityNodeInfo getRootNode(View embeddedView, int flutterId, Rect displayBounds) {
|
||||
return null;
|
||||
// TODO(amirh): uncomment this once a11y events and actions are wired.
|
||||
// AccessibilityNodeInfo originNode = embeddedView.createAccessibilityNodeInfo();
|
||||
// Long originPackedId = reflectionAccessors.getSourceNodeId(originNode);
|
||||
// if (originPackedId == null) {
|
||||
// return null;
|
||||
// }
|
||||
// int originId = ReflectionAccessors.getVirtualNodeId(originPackedId);
|
||||
// flutterIdToOrigin.put(flutterId, new ViewAndId(embeddedView, originId));
|
||||
// flutterIdToDisplayBounds.put(flutterId, displayBounds);
|
||||
// originToFlutterId.put(new ViewAndId(embeddedView, originId), flutterId);
|
||||
// return convertToFlutterNode(originNode, flutterId, embeddedView);
|
||||
public AccessibilityNodeInfo getRootNode(@NonNull View embeddedView, int flutterId, @NonNull Rect displayBounds) {
|
||||
AccessibilityNodeInfo originNode = embeddedView.createAccessibilityNodeInfo();
|
||||
Long originPackedId = reflectionAccessors.getSourceNodeId(originNode);
|
||||
if (originPackedId == null) {
|
||||
return null;
|
||||
}
|
||||
int originId = ReflectionAccessors.getVirtualNodeId(originPackedId);
|
||||
flutterIdToOrigin.put(flutterId, new ViewAndId(embeddedView, originId));
|
||||
flutterIdToDisplayBounds.put(flutterId, displayBounds);
|
||||
originToFlutterId.put(new ViewAndId(embeddedView, originId), flutterId);
|
||||
return convertToFlutterNode(originNode, flutterId, embeddedView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the accessibility node info for the node identified with `flutterId`.
|
||||
*/
|
||||
@Nullable
|
||||
public AccessibilityNodeInfo createAccessibilityNodeInfo(int flutterId) {
|
||||
ViewAndId origin = flutterIdToOrigin.get(flutterId);
|
||||
if (origin == null) {
|
||||
@ -101,6 +106,9 @@ class AccessibilityViewEmbedder {
|
||||
}
|
||||
AccessibilityNodeInfo originNode =
|
||||
origin.view.getAccessibilityNodeProvider().createAccessibilityNodeInfo(origin.id);
|
||||
if (originNode == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToFlutterNode(originNode, flutterId, origin.view);
|
||||
}
|
||||
|
||||
@ -108,7 +116,12 @@ class AccessibilityViewEmbedder {
|
||||
* Creates an AccessibilityNodeInfo that can be attached to the Flutter accessibility tree and is equivalent to
|
||||
* originNode(which belongs to embeddedView). The virtual ID for the created node will be flutterId.
|
||||
*/
|
||||
private AccessibilityNodeInfo convertToFlutterNode(AccessibilityNodeInfo originNode, int flutterId, View embeddedView) {
|
||||
@NonNull
|
||||
private AccessibilityNodeInfo convertToFlutterNode(
|
||||
@NonNull AccessibilityNodeInfo originNode,
|
||||
int flutterId,
|
||||
@NonNull View embeddedView
|
||||
) {
|
||||
AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(rootAccessibilityView, flutterId);
|
||||
result.setPackageName(rootAccessibilityView.getContext().getPackageName());
|
||||
result.setSource(rootAccessibilityView, flutterId);
|
||||
@ -124,7 +137,11 @@ class AccessibilityViewEmbedder {
|
||||
return result;
|
||||
}
|
||||
|
||||
private void setFlutterNodeParent(AccessibilityNodeInfo originNode, View embeddedView, AccessibilityNodeInfo result) {
|
||||
private void setFlutterNodeParent(
|
||||
@NonNull AccessibilityNodeInfo originNode,
|
||||
@NonNull View embeddedView,
|
||||
@NonNull AccessibilityNodeInfo result
|
||||
) {
|
||||
Long parentOriginPackedId = reflectionAccessors.getParentNodeId(originNode);
|
||||
if (parentOriginPackedId == null) {
|
||||
return;
|
||||
@ -137,7 +154,12 @@ class AccessibilityViewEmbedder {
|
||||
}
|
||||
|
||||
|
||||
private void addChildrenToFlutterNode(AccessibilityNodeInfo originNode, View embeddedView, Rect displayBounds, AccessibilityNodeInfo resultNode) {
|
||||
private void addChildrenToFlutterNode(
|
||||
@NonNull AccessibilityNodeInfo originNode,
|
||||
@NonNull View embeddedView,
|
||||
@NonNull Rect displayBounds,
|
||||
@NonNull AccessibilityNodeInfo resultNode
|
||||
) {
|
||||
for (int i = 0; i < originNode.getChildCount(); i++) {
|
||||
Long originPackedId = reflectionAccessors.getChildId(originNode, i);
|
||||
if (originPackedId == null) {
|
||||
@ -158,7 +180,11 @@ class AccessibilityViewEmbedder {
|
||||
}
|
||||
}
|
||||
|
||||
private void setFlutterNodesTranslateBounds(AccessibilityNodeInfo originNode, Rect displayBounds, AccessibilityNodeInfo resultNode) {
|
||||
private void setFlutterNodesTranslateBounds(
|
||||
@NonNull AccessibilityNodeInfo originNode,
|
||||
@NonNull Rect displayBounds,
|
||||
@NonNull AccessibilityNodeInfo resultNode
|
||||
) {
|
||||
Rect boundsInParent = new Rect();
|
||||
originNode.getBoundsInParent(boundsInParent);
|
||||
resultNode.setBoundsInParent(boundsInParent);
|
||||
@ -169,7 +195,7 @@ class AccessibilityViewEmbedder {
|
||||
resultNode.setBoundsInScreen(boundsInScreen);
|
||||
}
|
||||
|
||||
private void copyAccessibilityFields(AccessibilityNodeInfo input, AccessibilityNodeInfo output) {
|
||||
private void copyAccessibilityFields(@NonNull AccessibilityNodeInfo input, @NonNull AccessibilityNodeInfo output) {
|
||||
output.setAccessibilityFocused(input.isAccessibilityFocused());
|
||||
output.setCheckable(input.isCheckable());
|
||||
output.setChecked(input.isChecked());
|
||||
@ -220,6 +246,128 @@ class AccessibilityViewEmbedder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates an AccessibilityNodeProvider#requestSendAccessibilityEvent from the AccessibilityBridge to the embedded
|
||||
* view.
|
||||
*
|
||||
* @return True if the event was sent.
|
||||
*/
|
||||
public boolean requestSendAccessibilityEvent(
|
||||
@NonNull View embeddedView,
|
||||
@NonNull View eventOrigin,
|
||||
@NonNull AccessibilityEvent event
|
||||
) {
|
||||
AccessibilityEvent translatedEvent = AccessibilityEvent.obtain(event);
|
||||
Long originPackedId = reflectionAccessors.getRecordSourceNodeId(event);
|
||||
if (originPackedId == null) {
|
||||
return false;
|
||||
}
|
||||
int originVirtualId = ReflectionAccessors.getVirtualNodeId(originPackedId);
|
||||
Integer flutterId = originToFlutterId.get(new ViewAndId(embeddedView, originVirtualId));
|
||||
if (flutterId == null) {
|
||||
return false;
|
||||
}
|
||||
translatedEvent.setSource(rootAccessibilityView, flutterId);
|
||||
translatedEvent.setClassName(event.getClassName());
|
||||
translatedEvent.setPackageName(event.getPackageName());
|
||||
|
||||
for (int i = 0; i < translatedEvent.getRecordCount(); i++) {
|
||||
AccessibilityRecord record = translatedEvent.getRecord(i);
|
||||
Long recordOriginPackedId = reflectionAccessors.getRecordSourceNodeId(record);
|
||||
if (recordOriginPackedId == null) {
|
||||
return false;
|
||||
}
|
||||
int recordOriginVirtualID = ReflectionAccessors.getVirtualNodeId(recordOriginPackedId);
|
||||
ViewAndId originViewAndId = new ViewAndId(embeddedView, recordOriginVirtualID);
|
||||
if (!originToFlutterId.containsKey(originViewAndId)) {
|
||||
return false;
|
||||
}
|
||||
int recordFlutterId = originToFlutterId.get(originViewAndId);
|
||||
record.setSource(rootAccessibilityView, recordFlutterId);
|
||||
}
|
||||
|
||||
return rootAccessibilityView.getParent().requestSendAccessibilityEvent(eventOrigin, translatedEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates an @{link AccessibilityNodeProvider#performAction} from the AccessibilityBridge to the embedded view's
|
||||
* accessibility node provider.
|
||||
*
|
||||
* @return True if the action was performed.
|
||||
*/
|
||||
public boolean performAction(int flutterId, int accessibilityAction, @Nullable Bundle arguments) {
|
||||
ViewAndId origin = flutterIdToOrigin.get(flutterId);
|
||||
if (origin == null) {
|
||||
return false;
|
||||
}
|
||||
View embeddedView = origin.view;
|
||||
AccessibilityNodeProvider provider = embeddedView.getAccessibilityNodeProvider();
|
||||
if (provider == null) {
|
||||
return false;
|
||||
}
|
||||
return provider.performAction(origin.id, accessibilityAction, arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flutterID for an accessibility record, or null if no mapping exists.
|
||||
*
|
||||
* @param embeddedView the embedded view that the record is associated with.
|
||||
*/
|
||||
@Nullable
|
||||
public Integer getRecordFlutterId(@NonNull View embeddedView, @NonNull AccessibilityRecord record) {
|
||||
Long originPackedId = reflectionAccessors.getRecordSourceNodeId(record);
|
||||
if (originPackedId == null) {
|
||||
return null;
|
||||
}
|
||||
int originVirtualId = ReflectionAccessors.getVirtualNodeId(originPackedId);
|
||||
return originToFlutterId.get(new ViewAndId(embeddedView, originVirtualId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates a View#onHoverEvent event from the AccessibilityBridge to an embedded view.
|
||||
*
|
||||
* The pointer coordinates are translated to the embedded view's coordinate system.
|
||||
*/
|
||||
public boolean onAccessibilityHoverEvent(int rootFlutterId, @NonNull MotionEvent event) {
|
||||
ViewAndId origin = flutterIdToOrigin.get(rootFlutterId);
|
||||
if (origin == null) {
|
||||
return false;
|
||||
}
|
||||
Rect displayBounds = flutterIdToDisplayBounds.get(rootFlutterId);
|
||||
int pointerCount = event.getPointerCount();
|
||||
MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[pointerCount];
|
||||
MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
|
||||
for(int i = 0; i < event.getPointerCount(); i++) {
|
||||
pointerProperties[i] = new MotionEvent.PointerProperties();
|
||||
event.getPointerProperties(i, pointerProperties[i]);
|
||||
|
||||
MotionEvent.PointerCoords originCoords = new MotionEvent.PointerCoords();
|
||||
event.getPointerCoords(i, originCoords);
|
||||
|
||||
pointerCoords[i] = new MotionEvent.PointerCoords((originCoords));
|
||||
pointerCoords[i].x -= displayBounds.left;
|
||||
pointerCoords[i].y -= displayBounds.top;
|
||||
|
||||
}
|
||||
MotionEvent translatedEvent = MotionEvent.obtain(
|
||||
event.getDownTime(),
|
||||
event.getEventTime(),
|
||||
event.getAction(),
|
||||
event.getPointerCount(),
|
||||
pointerProperties,
|
||||
pointerCoords,
|
||||
event.getMetaState(),
|
||||
event.getButtonState(),
|
||||
event.getXPrecision(),
|
||||
event.getYPrecision(),
|
||||
event.getDeviceId(),
|
||||
event.getEdgeFlags(),
|
||||
event.getSource(),
|
||||
event.getFlags()
|
||||
);
|
||||
return origin.view.dispatchGenericMotionEvent(translatedEvent);
|
||||
}
|
||||
|
||||
private static class ViewAndId {
|
||||
final View view;
|
||||
final int id;
|
||||
@ -251,22 +399,29 @@ class AccessibilityViewEmbedder {
|
||||
private static class ReflectionAccessors {
|
||||
private final Method getSourceNodeId;
|
||||
private final Method getParentNodeId;
|
||||
private final Method getRecordSourceNodeId;
|
||||
private final Method getChildId;
|
||||
|
||||
private ReflectionAccessors() {
|
||||
Method getSourceNodeId = null;
|
||||
Method getParentNodeId = null;
|
||||
Method getRecordSourceNodeId = null;
|
||||
Method getChildId = null;
|
||||
try {
|
||||
getSourceNodeId = AccessibilityNodeInfo.class.getMethod("getSourceNodeId");
|
||||
} catch (NoSuchMethodException e) {
|
||||
Log.w(TAG, "can't invoke getSourceNodeId with reflection");
|
||||
Log.w(TAG, "can't invoke AccessibilityNodeInfo#getSourceNodeId with reflection");
|
||||
}
|
||||
try {
|
||||
getParentNodeId = AccessibilityNodeInfo.class.getMethod("getParentNodeId");
|
||||
} catch (NoSuchMethodException e) {
|
||||
Log.w(TAG, "can't invoke getParentNodeId with reflection");
|
||||
}
|
||||
try {
|
||||
getRecordSourceNodeId = AccessibilityRecord.class.getMethod("getSourceNodeId");
|
||||
} catch (NoSuchMethodException e) {
|
||||
Log.w(TAG, "can't invoke AccessibiiltyRecord#getSourceNodeId with reflection");
|
||||
}
|
||||
try {
|
||||
getChildId = AccessibilityNodeInfo.class.getMethod("getChildId", int.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
@ -274,6 +429,7 @@ class AccessibilityViewEmbedder {
|
||||
}
|
||||
this.getSourceNodeId = getSourceNodeId;
|
||||
this.getParentNodeId = getParentNodeId;
|
||||
this.getRecordSourceNodeId = getRecordSourceNodeId;
|
||||
this.getChildId = getChildId;
|
||||
}
|
||||
|
||||
@ -282,7 +438,8 @@ class AccessibilityViewEmbedder {
|
||||
return (int) (nodeId >> 32);
|
||||
}
|
||||
|
||||
private Long getSourceNodeId(AccessibilityNodeInfo node) {
|
||||
@Nullable
|
||||
private Long getSourceNodeId(@NonNull AccessibilityNodeInfo node) {
|
||||
if (getSourceNodeId == null) {
|
||||
return null;
|
||||
}
|
||||
@ -296,7 +453,8 @@ class AccessibilityViewEmbedder {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long getChildId(AccessibilityNodeInfo node, int child) {
|
||||
@Nullable
|
||||
private Long getChildId(@NonNull AccessibilityNodeInfo node, int child) {
|
||||
if (getChildId == null) {
|
||||
return null;
|
||||
}
|
||||
@ -310,7 +468,8 @@ class AccessibilityViewEmbedder {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long getParentNodeId(AccessibilityNodeInfo node) {
|
||||
@Nullable
|
||||
private Long getParentNodeId(@NonNull AccessibilityNodeInfo node) {
|
||||
if (getParentNodeId == null) {
|
||||
return null;
|
||||
}
|
||||
@ -323,5 +482,20 @@ class AccessibilityViewEmbedder {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Long getRecordSourceNodeId(@NonNull AccessibilityRecord node) {
|
||||
if (getRecordSourceNodeId == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (Long) getRecordSourceNodeId.invoke(node);
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.w(TAG, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user