Adding a code sample for using AccessibilityNodeProvider to report virtual Views.
Note: This is a sample and does *not* affect the system image, rather only the SDK. bug:5508317 Change-Id: I62bbef4b2a4c2789ddfa128e94ae37246d244ac0
This commit is contained in:
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.example.android.apis.accessibility;
|
||||
|
||||
import com.example.android.apis.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.accessibility.AccessibilityNodeProvider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This sample demonstrates how a View can expose a virtual view sub-tree
|
||||
* rooted at it. A virtual sub-tree is composed of imaginary Views
|
||||
* that are reported as a part of the view hierarchy for accessibility
|
||||
* purposes. This enables custom views that draw complex content to report
|
||||
* them selves as a tree of virtual views, thus conveying their logical
|
||||
* structure.
|
||||
* <p>
|
||||
* For example, a View may draw a monthly calendar as a grid of days while
|
||||
* each such day may contains some events. From a perspective of the View
|
||||
* hierarchy the calendar is composed of a single View but an accessibility
|
||||
* service would benefit of traversing the logical structure of the calendar
|
||||
* by examining each day and each event on that day.
|
||||
* </p>
|
||||
*/
|
||||
public class AccessibilityNodeProviderActivity extends Activity {
|
||||
/** Called when the activity is first created. */
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.accessibility_node_provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class presents a View that is composed of three virtual children
|
||||
* each of which is drawn with a different color and represents a region
|
||||
* of the View that has different semantics compared to other such regions.
|
||||
* While the virtual view tree exposed by this class is one level deep
|
||||
* for simplicity, there is no bound on the complexity of that virtual
|
||||
* sub-tree.
|
||||
*/
|
||||
public static class VirtualSubtreeRootView extends View {
|
||||
|
||||
/** Paint object for drawing the virtual sub-tree */
|
||||
private final Paint mPaint = new Paint();
|
||||
|
||||
/** Temporary rectangle to minimize object creation. */
|
||||
private final Rect mTempRect = new Rect();
|
||||
|
||||
/** Handle to the system accessibility service. */
|
||||
private final AccessibilityManager mAccessibilityManager;
|
||||
|
||||
/** The virtual children of this View. */
|
||||
private final List<VirtualView> mChildren = new ArrayList<VirtualView>();
|
||||
|
||||
/** The instance of the node provider for the virtual tree - lazily instantiated. */
|
||||
private AccessibilityNodeProvider mAccessibilityNodeProvider;
|
||||
|
||||
/** The last hovered child used for event dispatching. */
|
||||
private VirtualView mLastHoveredChild;
|
||||
|
||||
public VirtualSubtreeRootView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mAccessibilityManager = (AccessibilityManager) context.getSystemService(
|
||||
Service.ACCESSIBILITY_SERVICE);
|
||||
createVirtualChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
|
||||
// Instantiate the provide only when requested. Since the system
|
||||
// will call this method multiple times it is a good practice to
|
||||
// cache the provider instance.
|
||||
if (mAccessibilityNodeProvider == null) {
|
||||
mAccessibilityNodeProvider = new VirtualDescendantsProvider();
|
||||
}
|
||||
return mAccessibilityNodeProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean dispatchHoverEvent(MotionEvent event) {
|
||||
// This implementation assumes that the virtual children
|
||||
// cannot overlap and are always visible. Do NOT use this
|
||||
// code as a reference of how to implement hover event
|
||||
// dispatch. Instead, refer to ViewGroup#dispatchHoverEvent.
|
||||
boolean handled = false;
|
||||
List<VirtualView> children = mChildren;
|
||||
final int childCount = children.size();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
VirtualView child = children.get(i);
|
||||
Rect childBounds = child.mBounds;
|
||||
final int childCoordsX = (int) event.getX() + getScrollX();
|
||||
final int childCoordsY = (int) event.getY() + getScrollY();
|
||||
if (!childBounds.contains(childCoordsX, childCoordsY)) {
|
||||
continue;
|
||||
}
|
||||
final int action = event.getAction();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_HOVER_ENTER: {
|
||||
mLastHoveredChild = child;
|
||||
handled |= onHoverVirtualView(child, event);
|
||||
event.setAction(action);
|
||||
} break;
|
||||
case MotionEvent.ACTION_HOVER_MOVE: {
|
||||
if (child == mLastHoveredChild) {
|
||||
handled |= onHoverVirtualView(child, event);
|
||||
event.setAction(action);
|
||||
} else {
|
||||
MotionEvent eventNoHistory = event.getHistorySize() > 0
|
||||
? MotionEvent.obtainNoHistory(event) : event;
|
||||
eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT);
|
||||
onHoverVirtualView(mLastHoveredChild, eventNoHistory);
|
||||
eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER);
|
||||
onHoverVirtualView(child, eventNoHistory);
|
||||
mLastHoveredChild = child;
|
||||
eventNoHistory.setAction(MotionEvent.ACTION_HOVER_MOVE);
|
||||
handled |= onHoverVirtualView(child, eventNoHistory);
|
||||
if (eventNoHistory != event) {
|
||||
eventNoHistory.recycle();
|
||||
} else {
|
||||
event.setAction(action);
|
||||
}
|
||||
}
|
||||
} break;
|
||||
case MotionEvent.ACTION_HOVER_EXIT: {
|
||||
mLastHoveredChild = null;
|
||||
handled |= onHoverVirtualView(child, event);
|
||||
event.setAction(action);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
if (!handled) {
|
||||
handled |= onHoverEvent(event);
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
// The virtual children are ordered horizontally next to
|
||||
// each other and take the entire space of this View.
|
||||
int offsetX = 0;
|
||||
List<VirtualView> children = mChildren;
|
||||
final int childCount = children.size();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
VirtualView child = children.get(i);
|
||||
Rect childBounds = child.mBounds;
|
||||
childBounds.set(offsetX, 0, offsetX + childBounds.width(), childBounds.height());
|
||||
offsetX += childBounds.width();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
// The virtual children are ordered horizontally next to
|
||||
// each other and take the entire space of this View.
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
List<VirtualView> children = mChildren;
|
||||
final int childCount = children.size();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
VirtualView child = children.get(i);
|
||||
width += child.mBounds.width();
|
||||
height = Math.max(height, child.mBounds.height());
|
||||
}
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
// Draw the virtual children with the reusable Paint object
|
||||
// and with the bounds and color which are child specific.
|
||||
Rect drawingRect = mTempRect;
|
||||
List<VirtualView> children = mChildren;
|
||||
final int childCount = children.size();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
VirtualView child = children.get(i);
|
||||
drawingRect.set(child.mBounds);
|
||||
mPaint.setColor(child.mColor);
|
||||
mPaint.setAlpha(child.mAlpha);
|
||||
canvas.drawRect(drawingRect, mPaint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the virtual children of this View.
|
||||
*/
|
||||
private void createVirtualChildren() {
|
||||
// The virtual portion of the tree is one level deep. Note
|
||||
// that implementations can use any way of representing and
|
||||
// drawing virtual view.
|
||||
VirtualView firstChild = new VirtualView(0, new Rect(0, 0, 150, 150), Color.RED,
|
||||
"Virtual view 1");
|
||||
mChildren.add(firstChild);
|
||||
VirtualView secondChild = new VirtualView(1, new Rect(0, 0, 150, 150), Color.GREEN,
|
||||
"Virtual view 2");
|
||||
mChildren.add(secondChild);
|
||||
VirtualView thirdChild = new VirtualView(2, new Rect(0, 0, 150, 150), Color.BLUE,
|
||||
"Virtual view 3");
|
||||
mChildren.add(thirdChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected state of a virtual view.
|
||||
*
|
||||
* @param virtualView The virtual view whose selected state to set.
|
||||
* @param selected Whether the virtual view is selected.
|
||||
*/
|
||||
private void setVirtualViewSelected(VirtualView virtualView, boolean selected) {
|
||||
virtualView.mAlpha = selected ? VirtualView.ALPHA_SELECTED : VirtualView.ALPHA_NOT_SELECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a hover over a virtual view.
|
||||
*
|
||||
* @param virtualView The virtual view over which is hovered.
|
||||
* @param event The event to dispatch.
|
||||
* @return Whether the event was handled.
|
||||
*/
|
||||
private boolean onHoverVirtualView(VirtualView virtualView, MotionEvent event) {
|
||||
// The implementation of hover event dispatch can be implemented
|
||||
// in any way that is found suitable. However, each virtual View
|
||||
// should fire a corresponding accessibility event whose source
|
||||
// is that virtual view. Accessibility services get the event source
|
||||
// as the entry point of the APIs for querying the window content.
|
||||
final int action = event.getAction();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_HOVER_ENTER: {
|
||||
sendAccessibilityEventForVirtualView(virtualView,
|
||||
AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
|
||||
} break;
|
||||
case MotionEvent.ACTION_HOVER_EXIT: {
|
||||
sendAccessibilityEventForVirtualView(virtualView,
|
||||
AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
|
||||
} break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a properly initialized accessibility event for a virtual view..
|
||||
*
|
||||
* @param virtualView The virtual view.
|
||||
* @param eventType The type of the event to send.
|
||||
*/
|
||||
private void sendAccessibilityEventForVirtualView(VirtualView virtualView, int eventType) {
|
||||
// If touch exploration, i.e. the user gets feedback while touching
|
||||
// the screen, is enabled we fire accessibility events.
|
||||
if (mAccessibilityManager.isTouchExplorationEnabled()) {
|
||||
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
|
||||
event.setPackageName(getContext().getPackageName());
|
||||
event.setClassName(virtualView.getClass().getName());
|
||||
event.setSource(VirtualSubtreeRootView.this, virtualView.mId);
|
||||
event.getText().add(virtualView.mText);
|
||||
getParent().requestSendAccessibilityEvent(VirtualSubtreeRootView.this, event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a virtual view given its id.
|
||||
*
|
||||
* @param id The virtual view id.
|
||||
* @return The found virtual view.
|
||||
*/
|
||||
private VirtualView findVirtualViewById(int id) {
|
||||
List<VirtualView> children = mChildren;
|
||||
final int childCount = children.size();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
VirtualView child = children.get(i);
|
||||
if (child.mId == id) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a virtual View.
|
||||
*/
|
||||
private class VirtualView {
|
||||
public static final int ALPHA_SELECTED = 255;
|
||||
public static final int ALPHA_NOT_SELECTED = 127;
|
||||
|
||||
public final int mId;
|
||||
public final int mColor;
|
||||
public final Rect mBounds;
|
||||
public final String mText;
|
||||
public int mAlpha;
|
||||
|
||||
public VirtualView(int id, Rect bounds, int color, String text) {
|
||||
mId = id;
|
||||
mColor = color;
|
||||
mBounds = bounds;
|
||||
mText = text;
|
||||
mAlpha = ALPHA_NOT_SELECTED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the provider that exposes the virtual View tree to accessibility
|
||||
* services. From the perspective of an accessibility service the
|
||||
* {@link AccessibilityNodeInfo}s it receives while exploring the sub-tree
|
||||
* rooted at this View will be the same as the ones it received while
|
||||
* exploring a View containing a sub-tree composed of real Views.
|
||||
*/
|
||||
private class VirtualDescendantsProvider extends AccessibilityNodeProvider {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
|
||||
AccessibilityNodeInfo info = null;
|
||||
if (virtualViewId == View.NO_ID) {
|
||||
// We are requested to create an AccessibilityNodeInfo describing
|
||||
// this View, i.e. the root of the virtual sub-tree. Note that the
|
||||
// host View has an AccessibilityNodeProvider which means that this
|
||||
// provider is responsible for creating the node info for that root.
|
||||
info = AccessibilityNodeInfo.obtain(VirtualSubtreeRootView.this);
|
||||
onInitializeAccessibilityNodeInfo(info);
|
||||
// Add the virtual children of the root View.
|
||||
List<VirtualView> children = mChildren;
|
||||
final int childCount = children.size();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
VirtualView child = children.get(i);
|
||||
info.addChild(VirtualSubtreeRootView.this, child.mId);
|
||||
}
|
||||
} else {
|
||||
// Find the view that corresponds to the given id.
|
||||
VirtualView virtualView = findVirtualViewById(virtualViewId);
|
||||
if (virtualView == null) {
|
||||
return null;
|
||||
}
|
||||
// Obtain and initialize an AccessibilityNodeInfo with
|
||||
// information about the virtual view.
|
||||
info = AccessibilityNodeInfo.obtain();
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_SELECT);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);
|
||||
info.setPackageName(getContext().getPackageName());
|
||||
info.setClassName(virtualView.getClass().getName());
|
||||
info.setSource(VirtualSubtreeRootView.this, virtualViewId);
|
||||
info.setBoundsInParent(virtualView.mBounds);
|
||||
info.setParent(VirtualSubtreeRootView.this);
|
||||
info.setText(virtualView.mText);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched,
|
||||
int virtualViewId) {
|
||||
if (TextUtils.isEmpty(searched)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String searchedLowerCase = searched.toLowerCase();
|
||||
List<AccessibilityNodeInfo> result = null;
|
||||
if (virtualViewId == View.NO_ID) {
|
||||
// If the search is from the root, i.e. this View, go over the virtual
|
||||
// children and look for ones that contain the searched string since
|
||||
// this View does not contain text itself.
|
||||
List<VirtualView> children = mChildren;
|
||||
final int childCount = children.size();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
VirtualView child = children.get(i);
|
||||
String textToLowerCase = child.mText.toLowerCase();
|
||||
if (textToLowerCase.contains(searchedLowerCase)) {
|
||||
if (result == null) {
|
||||
result = new ArrayList<AccessibilityNodeInfo>();
|
||||
}
|
||||
result.add(createAccessibilityNodeInfo(child.mId));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the search is from a virtual view, find the view. Since the tree
|
||||
// is one level deep we add a node info for the child to the result if
|
||||
// the child contains the searched text.
|
||||
VirtualView virtualView = findVirtualViewById(virtualViewId);
|
||||
if (virtualView != null) {
|
||||
String textToLowerCase = virtualView.mText.toLowerCase();
|
||||
if (textToLowerCase.contains(searchedLowerCase)) {
|
||||
result = new ArrayList<AccessibilityNodeInfo>();
|
||||
result.add(createAccessibilityNodeInfo(virtualViewId));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean performAccessibilityAction(int action, int virtualViewId) {
|
||||
if (virtualViewId == View.NO_ID) {
|
||||
// Perform the action on the host View.
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfo.ACTION_SELECT:
|
||||
if (!isSelected()) {
|
||||
setSelected(true);
|
||||
return isSelected();
|
||||
}
|
||||
break;
|
||||
case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION:
|
||||
if (isSelected()) {
|
||||
setSelected(false);
|
||||
return !isSelected();
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Find the view that corresponds to the given id.
|
||||
VirtualView child = findVirtualViewById(virtualViewId);
|
||||
if (child == null) {
|
||||
return false;
|
||||
}
|
||||
// Perform the action on a virtual view.
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfo.ACTION_SELECT:
|
||||
setVirtualViewSelected(child, true);
|
||||
invalidate();
|
||||
return true;
|
||||
case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION:
|
||||
setVirtualViewSelected(child, false);
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<h3 id="ActionBar">Accessibility</h3>
|
||||
<dl>
|
||||
<dt><a href="AccessibilityNodeProviderActivity.html">Accessibility Node Provider</a></dt>
|
||||
<dd>Demonstrates how to develop an accessibility node provider which manages a virtual
|
||||
View tree reported to accessibility services. The virtual subtree is rooted at a View
|
||||
that draws complex content and reports itself as a tree of virtual views, thus conveying
|
||||
its logical structure.
|
||||
</dd>
|
||||
</dl>
|
||||
Reference in New Issue
Block a user