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;
39
40import com.example.android.supportv4.R;
41
42import java.util.ArrayList;
43import java.util.List;
44
45/**
46 * This example shows how to use the {@link ExploreByTouchHelper} class in the
47 * Android support library to add accessibility support to a custom view that
48 * represents multiple logical items.
49 * <p>
50 * The {@link ExploreByTouchHelper} class wraps
51 * {@link AccessibilityNodeProviderCompat} and simplifies exposing information
52 * about a custom view's logical structure to accessibility services.
53 * <p>
54 * The custom view in this example is responsible for:
55 * <ul>
56 * <li>Creating a helper class that extends {@link ExploreByTouchHelper}
57 * <li>Setting the helper as the accessibility delegate using
58 * {@link ViewCompat#setAccessibilityDelegate}
59 * <li>Dispatching hover events to the helper in {@link View#dispatchHoverEvent}
60 * </ul>
61 * <p>
62 * The helper class implementation in this example is responsible for:
63 * <ul>
64 * <li>Mapping hover event coordinates to logical items
65 * <li>Exposing information about logical items to accessibility services
66 * <li>Handling accessibility actions
67 * <ul>
68 */
69public class ExploreByTouchHelperActivity extends Activity {
70    @Override
71    protected void onCreate(Bundle savedInstanceState) {
72        super.onCreate(savedInstanceState);
73
74        setContentView(R.layout.explore_by_touch_helper);
75
76        final CustomView customView = findViewById(R.id.custom_view);
77
78        // Adds an item at the top-left quarter of the custom view.
79        customView.addItem(getString(R.string.sample_item_a), 0, 0, 0.5f, 0.5f);
80
81        // Adds an item at the bottom-right quarter of the custom view.
82        CustomView.CustomItem itemB =
83                customView.addItem(getString(R.string.sample_item_b), 0.5f, 0.5f, 1, 1);
84
85        // Add an item at the bottom quarter of Item B.
86        CustomView.CustomItem itemC =
87                customView.addItem(getString(R.string.sample_item_c), 0, 0.75f, 1, 1);
88        customView.setParentItem(itemC, itemB);
89
90        // Add an item at the left quarter of Item C.
91        CustomView.CustomItem itemD =
92                customView.addItem(getString(R.string.sample_item_d), 0, 0f, 0.25f, 1);
93        customView.setParentItem(itemD, itemC);
94
95        customView.layoutItems();
96    }
97
98    /**
99     * Simple custom view that draws rectangular items to the screen. Each item
100     * has a checked state that may be toggled by tapping on the item.
101     */
102    public static class CustomView extends View {
103        private static final int NO_ITEM = -1;
104
105        private final Paint mPaint = new Paint();
106        private final Rect mTempBounds = new Rect();
107        private final List<CustomItem> mItems = new ArrayList<CustomItem>();
108        private CustomViewTouchHelper mTouchHelper;
109
110        public CustomView(Context context, AttributeSet attrs) {
111            super(context, attrs);
112
113            // Set up accessibility helper class.
114            mTouchHelper = new CustomViewTouchHelper(this);
115            ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
116        }
117
118        @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
119        @Override
120        public boolean dispatchHoverEvent(MotionEvent event) {
121            // Always attempt to dispatch hover events to accessibility first.
122            if (mTouchHelper.dispatchHoverEvent(event)) {
123                return true;
124            }
125
126            return super.dispatchHoverEvent(event);
127        }
128
129        @Override
130        public boolean onTouchEvent(MotionEvent event) {
131            switch (event.getAction()) {
132                case MotionEvent.ACTION_DOWN:
133                    return true;
134                case MotionEvent.ACTION_UP:
135                    final int itemIndex = getItemIndexUnder(event.getX(), event.getY());
136                    if (itemIndex >= 0) {
137                        onItemClicked(itemIndex);
138                    }
139                    return true;
140            }
141
142            return super.onTouchEvent(event);
143        }
144
145        /**
146         * Adds an item to the custom view. The item is positioned relative to
147         * the custom view bounds and its descriptions is drawn at its center.
148         *
149         * @param description The item's description.
150         * @param top Top coordinate as a fraction of the parent height, range
151         *            is [0,1].
152         * @param left Left coordinate as a fraction of the parent width, range
153         *            is [0,1].
154         * @param bottom Bottom coordinate as a fraction of the parent height,
155         *            range is [0,1].
156         * @param right Right coordinate as a fraction of the parent width,
157         *            range is [0,1].
158         */
159        public CustomItem addItem(String description, float left, float top, float right,
160                                  float bottom) {
161            final CustomItem item = new CustomItem();
162            item.mId = mItems.size();
163            item.mBounds = new RectF(left, top, right, bottom);
164            item.mDescription = description;
165            item.mChecked = false;
166            mItems.add(item);
167            return item;
168        }
169
170        /**
171         * Sets the parent of an CustomItem.  This adjusts the bounds so that they are relative to
172         * the specified view, and initializes the parent and child info to point to each either.
173         * @param item CustomItem that will become a child node.
174         * @param parent CustomItem that will become the parent node.
175         */
176        public void setParentItem(CustomItem item, CustomItem parent) {
177            item.mParent = parent;
178            parent.mChildren.add(item.mId);
179        }
180
181        /**
182         * Walk the view hierarchy of each item and calculate mBoundsInRoot.
183         */
184        public void layoutItems() {
185            for (CustomItem item : mItems) {
186                layoutItem(item);
187            }
188        }
189
190        void layoutItem(CustomItem item) {
191            item.mBoundsInRoot = new RectF(item.mBounds);
192            CustomItem parent = item.mParent;
193            while (parent != null) {
194                RectF bounds = item.mBoundsInRoot;
195                item.mBoundsInRoot.set(parent.mBounds.left + bounds.left * parent.mBounds.width(),
196                        parent.mBounds.top + bounds.top * parent.mBounds.height(),
197                        parent.mBounds.left + bounds.right * parent.mBounds.width(),
198                        parent.mBounds.top + bounds.bottom * parent.mBounds.height());
199                parent = parent.mParent;
200            }
201        }
202
203        @Override
204        protected void onDraw(Canvas canvas) {
205            super.onDraw(canvas);
206
207            final Paint paint = mPaint;
208            final Rect bounds = mTempBounds;
209            final int height = getHeight();
210            final int width = getWidth();
211
212            for (CustomItem item : mItems) {
213                if (item.mParent == null) {
214                    paint.setColor(item.mChecked ? Color.RED : Color.BLUE);
215                } else {
216                    paint.setColor(item.mChecked ? Color.MAGENTA : Color.GREEN);
217                }
218                paint.setStyle(Style.FILL);
219                scaleRectF(item.mBoundsInRoot, bounds, width, height);
220                canvas.drawRect(bounds, paint);
221                paint.setColor(Color.WHITE);
222                paint.setTextAlign(Align.CENTER);
223                canvas.drawText(item.mDescription, bounds.centerX(), bounds.centerY(), paint);
224            }
225        }
226
227        protected boolean onItemClicked(int index) {
228            final CustomItem item = getItem(index);
229            if (item == null) {
230                return false;
231            }
232
233            item.mChecked = !item.mChecked;
234            invalidate();
235
236            // Since the item's checked state is exposed to accessibility
237            // services through its AccessibilityNodeInfo, we need to invalidate
238            // the item's virtual view. At some point in the future, the
239            // framework will obtain an updated version of the virtual view.
240            mTouchHelper.invalidateVirtualView(index);
241
242            // We also need to let the framework know what type of event
243            // happened. Accessibility services may use this event to provide
244            // appropriate feedback to the user.
245            mTouchHelper.sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);
246
247            return true;
248        }
249
250        protected int getItemIndexUnder(float x, float y) {
251            final float scaledX = (x / getWidth());
252            final float scaledY = (y / getHeight());
253            final int n = mItems.size();
254
255            // Search in reverse order, so that topmost items are selected first.
256            for (int i = n - 1; i >= 0; i--) {
257                final CustomItem item = mItems.get(i);
258                if (item.mBoundsInRoot.contains(scaledX, scaledY)) {
259                    return i;
260                }
261            }
262
263            return NO_ITEM;
264        }
265
266        protected CustomItem getItem(int index) {
267            if ((index < 0) || (index >= mItems.size())) {
268                return null;
269            }
270
271            return mItems.get(index);
272        }
273
274        protected static void scaleRectF(RectF in, Rect out, int width, int height) {
275            out.top = (int) (in.top * height);
276            out.bottom = (int) (in.bottom * height);
277            out.left = (int) (in.left * width);
278            out.right = (int) (in.right * width);
279        }
280
281        private class CustomViewTouchHelper extends ExploreByTouchHelper {
282            private final Rect mTempRect = new Rect();
283
284            public CustomViewTouchHelper(View forView) {
285                super(forView);
286            }
287
288            @Override
289            protected int getVirtualViewAt(float x, float y) {
290                // We also perform hit detection in onTouchEvent(), and we can
291                // reuse that logic here. This will ensure consistency whether
292                // accessibility is on or off.
293                final int index = getItemIndexUnder(x, y);
294                if (index == NO_ITEM) {
295                    return ExploreByTouchHelper.INVALID_ID;
296                }
297
298                return index;
299            }
300
301            @Override
302            protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
303                // Since every item should be visible, and since we're mapping
304                // directly from item index to virtual view id, we can add
305                // the index of every view that doesn't have a parent.
306                final int n = mItems.size();
307                for (int i = 0; i < n; i++) {
308                    if (mItems.get(i).mParent == null) {
309                        virtualViewIds.add(i);
310                    }
311                }
312            }
313
314            @Override
315            protected void onPopulateEventForVirtualView(
316                    int virtualViewId, AccessibilityEvent event) {
317                final CustomItem item = getItem(virtualViewId);
318                if (item == null) {
319                    throw new IllegalArgumentException("Invalid virtual view id");
320                }
321
322                // The event must be populated with text, either using
323                // getText().add() or setContentDescription(). Since the item's
324                // description is displayed visually, we'll add it to the event
325                // text. If it was only used for accessibility, we would use
326                // setContentDescription().
327                event.getText().add(item.mDescription);
328            }
329
330            @Override
331            protected void onPopulateNodeForVirtualView(
332                    int virtualViewId, AccessibilityNodeInfoCompat node) {
333                final CustomItem item = getItem(virtualViewId);
334                if (item == null) {
335                    throw new IllegalArgumentException("Invalid virtual view id");
336                }
337
338                // Node and event text and content descriptions are usually
339                // identical, so we'll use the exact same string as before.
340                node.setText(item.mDescription);
341
342                // Reported bounds should be consistent with those used to draw
343                // the item in onDraw(). They should also be consistent with the
344                // hit detection performed in getVirtualViewAt() and
345                // onTouchEvent().
346                final Rect bounds = mTempRect;
347                int height = getHeight();
348                int width = getWidth();
349                if (item.mParent != null) {
350                    width = (int) (width * item.mParent.mBoundsInRoot.width());
351                    height = (int) (height * item.mParent.mBoundsInRoot.height());
352                }
353                scaleRectF(item.mBounds, bounds, width, height);
354                node.setBoundsInParent(bounds);
355
356                // Since the user can tap an item, add the CLICK action. We'll
357                // need to handle this later in onPerformActionForVirtualView.
358                node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
359
360                // This item has a checked state.
361                node.setCheckable(true);
362                node.setChecked(item.mChecked);
363
364                // Setup the hierarchy.
365                if (item.mParent != null) {
366                    node.setParent(CustomView.this, item.mParent.mId);
367                }
368                for (Integer id : item.mChildren) {
369                    node.addChild(CustomView.this, id);
370                }
371            }
372
373            @Override
374            protected boolean onPerformActionForVirtualView(
375                    int virtualViewId, int action, Bundle arguments) {
376                switch (action) {
377                    case AccessibilityNodeInfoCompat.ACTION_CLICK:
378                        // Click handling should be consistent with
379                        // onTouchEvent(). This ensures that the view works the
380                        // same whether accessibility is turned on or off.
381                        return onItemClicked(virtualViewId);
382                }
383
384                return false;
385            }
386
387        }
388
389        public static class CustomItem {
390            private int mId;
391            private CustomItem mParent;
392            private List<Integer> mChildren = new ArrayList<>();
393            private String mDescription;
394            private RectF mBounds;
395            private RectF mBoundsInRoot;
396            private boolean mChecked;
397        }
398    }
399}
400