Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.supportv4.widget;
     18 
     19 import android.annotation.TargetApi;
     20 import android.app.Activity;
     21 import android.content.Context;
     22 import android.graphics.Canvas;
     23 import android.graphics.Color;
     24 import android.graphics.Paint;
     25 import android.graphics.Paint.Align;
     26 import android.graphics.Paint.Style;
     27 import android.graphics.Rect;
     28 import android.graphics.RectF;
     29 import android.os.Build;
     30 import android.os.Bundle;
     31 import android.support.v4.view.ViewCompat;
     32 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     33 import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
     34 import android.support.v4.widget.ExploreByTouchHelper;
     35 import android.util.AttributeSet;
     36 import android.view.MotionEvent;
     37 import android.view.View;
     38 import android.view.accessibility.AccessibilityEvent;
     39 import com.example.android.supportv4.R;
     40 
     41 import java.util.ArrayList;
     42 import java.util.List;
     43 
     44 /**
     45  * This example shows how to use the {@link ExploreByTouchHelper} class in the
     46  * Android support library to add accessibility support to a custom view that
     47  * represents multiple logical items.
     48  * <p>
     49  * The {@link ExploreByTouchHelper} class wraps
     50  * {@link AccessibilityNodeProviderCompat} and simplifies exposing information
     51  * about a custom view's logical structure to accessibility services.
     52  * <p>
     53  * The custom view in this example is responsible for:
     54  * <ul>
     55  * <li>Creating a helper class that extends {@link ExploreByTouchHelper}
     56  * <li>Setting the helper as the accessibility delegate using
     57  * {@link ViewCompat#setAccessibilityDelegate}
     58  * <li>Dispatching hover events to the helper in {@link View#dispatchHoverEvent}
     59  * </ul>
     60  * <p>
     61  * The helper class implementation in this example is responsible for:
     62  * <ul>
     63  * <li>Mapping hover event coordinates to logical items
     64  * <li>Exposing information about logical items to accessibility services
     65  * <li>Handling accessibility actions
     66  * <ul>
     67  */
     68 public class ExploreByTouchHelperActivity extends Activity {
     69     @Override
     70     protected void onCreate(Bundle savedInstanceState) {
     71         super.onCreate(savedInstanceState);
     72 
     73         setContentView(R.layout.explore_by_touch_helper);
     74 
     75         final CustomView customView = (CustomView) findViewById(R.id.custom_view);
     76 
     77         // Adds an item at the top-left quarter of the custom view.
     78         customView.addItem(getString(R.string.sample_item_a), 0, 0, 0.5f, 0.5f);
     79 
     80         // Adds an item at the bottom-right quarter of the custom view.
     81         customView.addItem(getString(R.string.sample_item_b), 0.5f, 0.5f, 1, 1);
     82     }
     83 
     84     /**
     85      * Simple custom view that draws rectangular items to the screen. Each item
     86      * has a checked state that may be toggled by tapping on the item.
     87      */
     88     public static class CustomView extends View {
     89         private static final int NO_ITEM = -1;
     90 
     91         private final Paint mPaint = new Paint();
     92         private final Rect mTempBounds = new Rect();
     93         private final List<CustomItem> mItems = new ArrayList<CustomItem>();
     94         private CustomViewTouchHelper mTouchHelper;
     95 
     96         public CustomView(Context context, AttributeSet attrs) {
     97             super(context, attrs);
     98 
     99             // Set up accessibility helper class.
    100             mTouchHelper = new CustomViewTouchHelper(this);
    101             ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
    102         }
    103 
    104         @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    105         @Override
    106         public boolean dispatchHoverEvent(MotionEvent event) {
    107             // Always attempt to dispatch hover events to accessibility first.
    108             if (mTouchHelper.dispatchHoverEvent(event)) {
    109                 return true;
    110             }
    111 
    112             return super.dispatchHoverEvent(event);
    113         }
    114 
    115         @Override
    116         public boolean onTouchEvent(MotionEvent event) {
    117             switch (event.getAction()) {
    118                 case MotionEvent.ACTION_DOWN:
    119                     return true;
    120                 case MotionEvent.ACTION_UP:
    121                     final int itemIndex = getItemIndexUnder(event.getX(), event.getY());
    122                     if (itemIndex >= 0) {
    123                         onItemClicked(itemIndex);
    124                     }
    125                     return true;
    126             }
    127 
    128             return super.onTouchEvent(event);
    129         }
    130 
    131         /**
    132          * Adds an item to the custom view. The item is positioned relative to
    133          * the custom view bounds and its descriptions is drawn at its center.
    134          *
    135          * @param description The item's description.
    136          * @param top Top coordinate as a fraction of the parent height, range
    137          *            is [0,1].
    138          * @param left Left coordinate as a fraction of the parent width, range
    139          *            is [0,1].
    140          * @param bottom Bottom coordinate as a fraction of the parent height,
    141          *            range is [0,1].
    142          * @param right Right coordinate as a fraction of the parent width,
    143          *            range is [0,1].
    144          */
    145         public void addItem(String description, float top, float left, float bottom, float right) {
    146             final CustomItem item = new CustomItem();
    147             item.bounds = new RectF(top, left, bottom, right);
    148             item.description = description;
    149             item.checked = false;
    150             mItems.add(item);
    151         }
    152 
    153         @Override
    154         protected void onDraw(Canvas canvas) {
    155             super.onDraw(canvas);
    156 
    157             final Paint paint = mPaint;
    158             final Rect bounds = mTempBounds;
    159             final int height = getHeight();
    160             final int width = getWidth();
    161 
    162             for (CustomItem item : mItems) {
    163                 paint.setColor(item.checked ? Color.RED : Color.BLUE);
    164                 paint.setStyle(Style.FILL);
    165                 scaleRectF(item.bounds, bounds, width, height);
    166                 canvas.drawRect(bounds, paint);
    167                 paint.setColor(Color.WHITE);
    168                 paint.setTextAlign(Align.CENTER);
    169                 canvas.drawText(item.description, bounds.centerX(), bounds.centerY(), paint);
    170             }
    171         }
    172 
    173         protected boolean onItemClicked(int index) {
    174             final CustomItem item = getItem(index);
    175             if (item == null) {
    176                 return false;
    177             }
    178 
    179             item.checked = !item.checked;
    180             invalidate();
    181 
    182             // Since the item's checked state is exposed to accessibility
    183             // services through its AccessibilityNodeInfo, we need to invalidate
    184             // the item's virtual view. At some point in the future, the
    185             // framework will obtain an updated version of the virtual view.
    186             mTouchHelper.invalidateVirtualView(index);
    187 
    188             // We also need to let the framework know what type of event
    189             // happened. Accessibility services may use this event to provide
    190             // appropriate feedback to the user.
    191             mTouchHelper.sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);
    192 
    193             return true;
    194         }
    195 
    196         protected int getItemIndexUnder(float x, float y) {
    197             final float scaledX = (x / getWidth());
    198             final float scaledY = (y / getHeight());
    199             final int n = mItems.size();
    200 
    201             for (int i = 0; i < n; i++) {
    202                 final CustomItem item = mItems.get(i);
    203                 if (item.bounds.contains(scaledX, scaledY)) {
    204                     return i;
    205                 }
    206             }
    207 
    208             return NO_ITEM;
    209         }
    210 
    211         protected CustomItem getItem(int index) {
    212             if ((index < 0) || (index >= mItems.size())) {
    213                 return null;
    214             }
    215 
    216             return mItems.get(index);
    217         }
    218 
    219         protected static void scaleRectF(RectF in, Rect out, int width, int height) {
    220             out.top = (int) (in.top * height);
    221             out.bottom = (int) (in.bottom * height);
    222             out.left = (int) (in.left * width);
    223             out.right = (int) (in.right * width);
    224         }
    225 
    226         private class CustomViewTouchHelper extends ExploreByTouchHelper {
    227             private final Rect mTempRect = new Rect();
    228 
    229             public CustomViewTouchHelper(View forView) {
    230                 super(forView);
    231             }
    232 
    233             @Override
    234             protected int getVirtualViewAt(float x, float y) {
    235                 // We also perform hit detection in onTouchEvent(), and we can
    236                 // reuse that logic here. This will ensure consistency whether
    237                 // accessibility is on or off.
    238                 final int index = getItemIndexUnder(x, y);
    239                 if (index == NO_ITEM) {
    240                     return ExploreByTouchHelper.INVALID_ID;
    241                 }
    242 
    243                 return index;
    244             }
    245 
    246             @Override
    247             protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
    248                 // Since every item should be visible, and since we're mapping
    249                 // directly from item index to virtual view id, we can just add
    250                 // every available index in the item list.
    251                 final int n = mItems.size();
    252                 for (int i = 0; i < n; i++) {
    253                     virtualViewIds.add(i);
    254                 }
    255             }
    256 
    257             @Override
    258             protected void onPopulateEventForVirtualView(
    259                     int virtualViewId, AccessibilityEvent event) {
    260                 final CustomItem item = getItem(virtualViewId);
    261                 if (item == null) {
    262                     throw new IllegalArgumentException("Invalid virtual view id");
    263                 }
    264 
    265                 // The event must be populated with text, either using
    266                 // getText().add() or setContentDescription(). Since the item's
    267                 // description is displayed visually, we'll add it to the event
    268                 // text. If it was only used for accessibility, we would use
    269                 // setContentDescription().
    270                 event.getText().add(item.description);
    271             }
    272 
    273             @Override
    274             protected void onPopulateNodeForVirtualView(
    275                     int virtualViewId, AccessibilityNodeInfoCompat node) {
    276                 final CustomItem item = getItem(virtualViewId);
    277                 if (item == null) {
    278                     throw new IllegalArgumentException("Invalid virtual view id");
    279                 }
    280 
    281                 // Node and event text and content descriptions are usually
    282                 // identical, so we'll use the exact same string as before.
    283                 node.setText(item.description);
    284 
    285                 // Reported bounds should be consistent with those used to draw
    286                 // the item in onDraw(). They should also be consistent with the
    287                 // hit detection performed in getVirtualViewAt() and
    288                 // onTouchEvent().
    289                 final Rect bounds = mTempRect;
    290                 final int height = getHeight();
    291                 final int width = getWidth();
    292                 scaleRectF(item.bounds, bounds, width, height);
    293                 node.setBoundsInParent(bounds);
    294 
    295                 // Since the user can tap an item, add the CLICK action. We'll
    296                 // need to handle this later in onPerformActionForVirtualView.
    297                 node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
    298 
    299                 // This item has a checked state.
    300                 node.setCheckable(true);
    301                 node.setChecked(item.checked);
    302             }
    303 
    304             @Override
    305             protected boolean onPerformActionForVirtualView(
    306                     int virtualViewId, int action, Bundle arguments) {
    307                 switch (action) {
    308                     case AccessibilityNodeInfoCompat.ACTION_CLICK:
    309                         // Click handling should be consistent with
    310                         // onTouchEvent(). This ensures that the view works the
    311                         // same whether accessibility is turned on or off.
    312                         return onItemClicked(virtualViewId);
    313                 }
    314 
    315                 return false;
    316             }
    317 
    318         }
    319 
    320         public static class CustomItem {
    321             private String description;
    322             private RectF bounds;
    323             private boolean checked;
    324         }
    325     }
    326 }
    327