• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onCreate(Bundle savedInstanceState)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 
CustomView(Context context, AttributeSet attrs)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
dispatchHoverEvent(MotionEvent event)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
onTouchEvent(MotionEvent event)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          */
addItem(String description, float top, float left, float bottom, float right)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
onDraw(Canvas canvas)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 
onItemClicked(int index)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 
getItemIndexUnder(float x, float y)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 
getItem(int index)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 
scaleRectF(RectF in, Rect out, int width, int height)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 
CustomViewTouchHelper(View forView)229             public CustomViewTouchHelper(View forView) {
230                 super(forView);
231             }
232 
233             @Override
getVirtualViewAt(float x, float y)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
getVisibleVirtualViews(List<Integer> virtualViewIds)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
onPopulateEventForVirtualView( int virtualViewId, AccessibilityEvent event)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
onPopulateNodeForVirtualView( int virtualViewId, AccessibilityNodeInfoCompat node)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
onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments)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