blob: 1ca036a551b25a6cad2a998e9b41018bcfd6f0e1 [file] [log] [blame]
/*
* 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 performAction(int virtualViewId, int action, Bundle arguments) {
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;
}
}
}
}