/* * Copyright (C) 2013 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.supportv4.widget; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; import android.os.Bundle; import android.support.v4.view.ViewCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; import android.support.v4.widget.ExploreByTouchHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import com.example.android.supportv4.R; import java.util.ArrayList; import java.util.List; /** * This example shows how to use the {@link ExploreByTouchHelper} class in the * Android support library to add accessibility support to a custom view that * represents multiple logical items. * <p> * The {@link ExploreByTouchHelper} class wraps * {@link AccessibilityNodeProviderCompat} and simplifies exposing information * about a custom view's logical structure to accessibility services. * <p> * The custom view in this example is responsible for: * <ul> * <li>Creating a helper class that extends {@link ExploreByTouchHelper} * <li>Setting the helper as the accessibility delegate using * {@link ViewCompat#setAccessibilityDelegate} * <li>Dispatching hover events to the helper in {@link View#dispatchHoverEvent} * </ul> * <p> * The helper class implementation in this example is responsible for: * <ul> * <li>Mapping hover event coordinates to logical items * <li>Exposing information about logical items to accessibility services * <li>Handling accessibility actions * <ul> */ public class ExploreByTouchHelperActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.explore_by_touch_helper); final CustomView customView = (CustomView) findViewById(R.id.custom_view); // Adds an item at the top-left quarter of the custom view. customView.addItem(getString(R.string.sample_item_a), 0, 0, 0.5f, 0.5f); // Adds an item at the bottom-right quarter of the custom view. customView.addItem(getString(R.string.sample_item_b), 0.5f, 0.5f, 1, 1); } /** * Simple custom view that draws rectangular items to the screen. Each item * has a checked state that may be toggled by tapping on the item. */ public static class CustomView extends View { private static final int NO_ITEM = -1; private final Paint mPaint = new Paint(); private final Rect mTempBounds = new Rect(); private final List<CustomItem> mItems = new ArrayList<CustomItem>(); private CustomViewTouchHelper mTouchHelper; public CustomView(Context context, AttributeSet attrs) { super(context, attrs); // Set up accessibility helper class. mTouchHelper = new CustomViewTouchHelper(this); ViewCompat.setAccessibilityDelegate(this, mTouchHelper); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public boolean dispatchHoverEvent(MotionEvent event) { // Always attempt to dispatch hover events to accessibility first. if (mTouchHelper.dispatchHoverEvent(event)) { return true; } return super.dispatchHoverEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true; case MotionEvent.ACTION_UP: final int itemIndex = getItemIndexUnder(event.getX(), event.getY()); if (itemIndex >= 0) { onItemClicked(itemIndex); } return true; } return super.onTouchEvent(event); } /** * Adds an item to the custom view. The item is positioned relative to * the custom view bounds and its descriptions is drawn at its center. * * @param description The item's description. * @param top Top coordinate as a fraction of the parent height, range * is [0,1]. * @param left Left coordinate as a fraction of the parent width, range * is [0,1]. * @param bottom Bottom coordinate as a fraction of the parent height, * range is [0,1]. * @param right Right coordinate as a fraction of the parent width, * range is [0,1]. */ public void addItem(String description, float top, float left, float bottom, float right) { final CustomItem item = new CustomItem(); item.bounds = new RectF(top, left, bottom, right); item.description = description; item.checked = false; mItems.add(item); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); final Paint paint = mPaint; final Rect bounds = mTempBounds; final int height = getHeight(); final int width = getWidth(); for (CustomItem item : mItems) { paint.setColor(item.checked ? Color.RED : Color.BLUE); paint.setStyle(Style.FILL); scaleRectF(item.bounds, bounds, width, height); canvas.drawRect(bounds, paint); paint.setColor(Color.WHITE); paint.setTextAlign(Align.CENTER); canvas.drawText(item.description, bounds.centerX(), bounds.centerY(), paint); } } protected boolean onItemClicked(int index) { final CustomItem item = getItem(index); if (item == null) { return false; } item.checked = !item.checked; invalidate(); // Since the item's checked state is exposed to accessibility // services through its AccessibilityNodeInfo, we need to invalidate // the item's virtual view. At some point in the future, the // framework will obtain an updated version of the virtual view. mTouchHelper.invalidateVirtualView(index); // We also need to let the framework know what type of event // happened. Accessibility services may use this event to provide // appropriate feedback to the user. mTouchHelper.sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED); return true; } protected int getItemIndexUnder(float x, float y) { final float scaledX = (x / getWidth()); final float scaledY = (y / getHeight()); final int n = mItems.size(); for (int i = 0; i < n; i++) { final CustomItem item = mItems.get(i); if (item.bounds.contains(scaledX, scaledY)) { return i; } } return NO_ITEM; } protected CustomItem getItem(int index) { if ((index < 0) || (index >= mItems.size())) { return null; } return mItems.get(index); } protected static void scaleRectF(RectF in, Rect out, int width, int height) { out.top = (int) (in.top * height); out.bottom = (int) (in.bottom * height); out.left = (int) (in.left * width); out.right = (int) (in.right * width); } private class CustomViewTouchHelper extends ExploreByTouchHelper { private final Rect mTempRect = new Rect(); public CustomViewTouchHelper(View forView) { super(forView); } @Override protected int getVirtualViewAt(float x, float y) { // We also perform hit detection in onTouchEvent(), and we can // reuse that logic here. This will ensure consistency whether // accessibility is on or off. final int index = getItemIndexUnder(x, y); if (index == NO_ITEM) { return ExploreByTouchHelper.INVALID_ID; } return index; } @Override protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { // Since every item should be visible, and since we're mapping // directly from item index to virtual view id, we can just add // every available index in the item list. final int n = mItems.size(); for (int i = 0; i < n; i++) { virtualViewIds.add(i); } } @Override protected void onPopulateEventForVirtualView( int virtualViewId, AccessibilityEvent event) { final CustomItem item = getItem(virtualViewId); if (item == null) { throw new IllegalArgumentException("Invalid virtual view id"); } // The event must be populated with text, either using // getText().add() or setContentDescription(). Since the item's // description is displayed visually, we'll add it to the event // text. If it was only used for accessibility, we would use // setContentDescription(). event.getText().add(item.description); } @Override protected void onPopulateNodeForVirtualView( int virtualViewId, AccessibilityNodeInfoCompat node) { final CustomItem item = getItem(virtualViewId); if (item == null) { throw new IllegalArgumentException("Invalid virtual view id"); } // Node and event text and content descriptions are usually // identical, so we'll use the exact same string as before. node.setText(item.description); // Reported bounds should be consistent with those used to draw // the item in onDraw(). They should also be consistent with the // hit detection performed in getVirtualViewAt() and // onTouchEvent(). final Rect bounds = mTempRect; final int height = getHeight(); final int width = getWidth(); scaleRectF(item.bounds, bounds, width, height); node.setBoundsInParent(bounds); // Since the user can tap an item, add the CLICK action. We'll // need to handle this later in onPerformActionForVirtualView. node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); // This item has a checked state. node.setCheckable(true); node.setChecked(item.checked); } @Override protected boolean onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments) { switch (action) { case AccessibilityNodeInfoCompat.ACTION_CLICK: // Click handling should be consistent with // onTouchEvent(). This ensures that the view works the // same whether accessibility is turned on or off. return onItemClicked(virtualViewId); } return false; } } public static class CustomItem { private String description; private RectF bounds; private boolean checked; } } }