ExploreByTouchHelperActivity.java revision 11d03f96a3f29508c703b3df60a634eeaba5d37d
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
17package com.example.android.supportv4.widget;
18
19import android.annotation.TargetApi;
20import android.app.Activity;
21import android.content.Context;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.Paint;
25import android.graphics.Paint.Align;
26import android.graphics.Paint.Style;
27import android.graphics.Rect;
28import android.graphics.RectF;
29import android.os.Build;
30import android.os.Bundle;
31import android.support.v4.view.ViewCompat;
32import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
33import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
34import android.support.v4.widget.ExploreByTouchHelper;
35import android.util.AttributeSet;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.accessibility.AccessibilityEvent;
39import com.example.android.supportv4.R;
40
41import java.util.ArrayList;
42import 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 */
68public 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