111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette/*
211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * Copyright (C) 2013 The Android Open Source Project
311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette *
411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * Licensed under the Apache License, Version 2.0 (the "License");
511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * you may not use this file except in compliance with the License.
611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * You may obtain a copy of the License at
711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette *
811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette *      http://www.apache.org/licenses/LICENSE-2.0
911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette *
1011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * Unless required by applicable law or agreed to in writing, software
1111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * distributed under the License is distributed on an "AS IS" BASIS,
1211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * See the License for the specific language governing permissions and
1411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * limitations under the License.
1511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette */
1611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
1711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverettepackage com.example.android.supportv4.widget;
1811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
1911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.annotation.TargetApi;
2011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.app.Activity;
2111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.content.Context;
2211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.graphics.Canvas;
2311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.graphics.Color;
2411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.graphics.Paint;
2511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.graphics.Paint.Align;
2611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.graphics.Paint.Style;
2711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.graphics.Rect;
2811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.graphics.RectF;
2911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.os.Build;
3011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.os.Bundle;
3111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.support.v4.view.ViewCompat;
3211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
3311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
3411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.support.v4.widget.ExploreByTouchHelper;
3511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.util.AttributeSet;
3611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.view.MotionEvent;
3711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.view.View;
3811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport android.view.accessibility.AccessibilityEvent;
3946f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia
4011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport com.example.android.supportv4.R;
4111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
4211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport java.util.ArrayList;
4311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viveretteimport java.util.List;
4411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
4511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette/**
4611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * This example shows how to use the {@link ExploreByTouchHelper} class in the
4711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * Android support library to add accessibility support to a custom view that
4811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * represents multiple logical items.
4911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <p>
5011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * The {@link ExploreByTouchHelper} class wraps
5111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * {@link AccessibilityNodeProviderCompat} and simplifies exposing information
5211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * about a custom view's logical structure to accessibility services.
5311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <p>
5411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * The custom view in this example is responsible for:
5511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <ul>
5611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <li>Creating a helper class that extends {@link ExploreByTouchHelper}
5711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <li>Setting the helper as the accessibility delegate using
5811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * {@link ViewCompat#setAccessibilityDelegate}
5911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <li>Dispatching hover events to the helper in {@link View#dispatchHoverEvent}
6011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * </ul>
6111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <p>
6211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * The helper class implementation in this example is responsible for:
6311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <ul>
6411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <li>Mapping hover event coordinates to logical items
6511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <li>Exposing information about logical items to accessibility services
6611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <li>Handling accessibility actions
6711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette * <ul>
6811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette */
6911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverettepublic class ExploreByTouchHelperActivity extends Activity {
7011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette    @Override
7111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette    protected void onCreate(Bundle savedInstanceState) {
7211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        super.onCreate(savedInstanceState);
7311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
7411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        setContentView(R.layout.explore_by_touch_helper);
7511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
76fa2e2acf79d791a90410025daad438968550d18cAlan Viverette        final CustomView customView = findViewById(R.id.custom_view);
7711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
7811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        // Adds an item at the top-left quarter of the custom view.
7911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        customView.addItem(getString(R.string.sample_item_a), 0, 0, 0.5f, 0.5f);
8011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
8111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        // Adds an item at the bottom-right quarter of the custom view.
8246f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia        CustomView.CustomItem itemB =
8346f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                customView.addItem(getString(R.string.sample_item_b), 0.5f, 0.5f, 1, 1);
8446f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia
8546f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia        // Add an item at the bottom quarter of Item B.
8646f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia        CustomView.CustomItem itemC =
8746f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                customView.addItem(getString(R.string.sample_item_c), 0, 0.75f, 1, 1);
8846f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia        customView.setParentItem(itemC, itemB);
893b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia
903b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia        // Add an item at the left quarter of Item C.
913b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia        CustomView.CustomItem itemD =
923b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                customView.addItem(getString(R.string.sample_item_d), 0, 0f, 0.25f, 1);
933b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia        customView.setParentItem(itemD, itemC);
943b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia
953b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia        customView.layoutItems();
9611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette    }
9711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
9811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette    /**
9911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette     * Simple custom view that draws rectangular items to the screen. Each item
10011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette     * has a checked state that may be toggled by tapping on the item.
10111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette     */
10211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette    public static class CustomView extends View {
10311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        private static final int NO_ITEM = -1;
10411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
10511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        private final Paint mPaint = new Paint();
10611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        private final Rect mTempBounds = new Rect();
10711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        private final List<CustomItem> mItems = new ArrayList<CustomItem>();
10811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        private CustomViewTouchHelper mTouchHelper;
10911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
11011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        public CustomView(Context context, AttributeSet attrs) {
11111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            super(context, attrs);
11211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
11311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            // Set up accessibility helper class.
11411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            mTouchHelper = new CustomViewTouchHelper(this);
11511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
11611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
11711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
11811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
11911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        @Override
12011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        public boolean dispatchHoverEvent(MotionEvent event) {
12111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            // Always attempt to dispatch hover events to accessibility first.
12211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            if (mTouchHelper.dispatchHoverEvent(event)) {
12311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                return true;
12411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
12511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
12611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            return super.dispatchHoverEvent(event);
12711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
12811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
12911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        @Override
13011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        public boolean onTouchEvent(MotionEvent event) {
13111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            switch (event.getAction()) {
13211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                case MotionEvent.ACTION_DOWN:
13311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    return true;
13411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                case MotionEvent.ACTION_UP:
13511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    final int itemIndex = getItemIndexUnder(event.getX(), event.getY());
13611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    if (itemIndex >= 0) {
13711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                        onItemClicked(itemIndex);
13811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    }
13911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    return true;
14011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
14111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
14211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            return super.onTouchEvent(event);
14311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
14411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
14511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        /**
14611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         * Adds an item to the custom view. The item is positioned relative to
14711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         * the custom view bounds and its descriptions is drawn at its center.
14811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         *
14911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         * @param description The item's description.
15011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         * @param top Top coordinate as a fraction of the parent height, range
15111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         *            is [0,1].
15211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         * @param left Left coordinate as a fraction of the parent width, range
15311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         *            is [0,1].
15411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         * @param bottom Bottom coordinate as a fraction of the parent height,
15511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         *            range is [0,1].
15611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         * @param right Right coordinate as a fraction of the parent width,
15711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         *            range is [0,1].
15811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette         */
15946f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia        public CustomItem addItem(String description, float left, float top, float right,
16046f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                                  float bottom) {
16111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            final CustomItem item = new CustomItem();
16246f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            item.mId = mItems.size();
16346f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            item.mBounds = new RectF(left, top, right, bottom);
16446f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            item.mDescription = description;
16546f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            item.mChecked = false;
16611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            mItems.add(item);
16746f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            return item;
16846f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia        }
16946f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia
17046f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia        /**
17146f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia         * Sets the parent of an CustomItem.  This adjusts the bounds so that they are relative to
17246f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia         * the specified view, and initializes the parent and child info to point to each either.
17346f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia         * @param item CustomItem that will become a child node.
17446f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia         * @param parent CustomItem that will become the parent node.
17546f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia         */
17646f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia        public void setParentItem(CustomItem item, CustomItem parent) {
17746f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            item.mParent = parent;
17846f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            parent.mChildren.add(item.mId);
1793b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia        }
18046f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia
1813b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia        /**
1823b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia         * Walk the view hierarchy of each item and calculate mBoundsInRoot.
1833b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia         */
1843b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia        public void layoutItems() {
1853b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia            for (CustomItem item : mItems) {
1863b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                layoutItem(item);
1873b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia            }
1883b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia        }
1893b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia
1903b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia        void layoutItem(CustomItem item) {
1913b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia            item.mBoundsInRoot = new RectF(item.mBounds);
1923b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia            CustomItem parent = item.mParent;
1933b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia            while (parent != null) {
1943b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                RectF bounds = item.mBoundsInRoot;
1953b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                item.mBoundsInRoot.set(parent.mBounds.left + bounds.left * parent.mBounds.width(),
1963b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                        parent.mBounds.top + bounds.top * parent.mBounds.height(),
1973b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                        parent.mBounds.left + bounds.right * parent.mBounds.width(),
1983b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                        parent.mBounds.top + bounds.bottom * parent.mBounds.height());
1993b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                parent = parent.mParent;
2003b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia            }
20111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
20211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
20311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        @Override
20411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        protected void onDraw(Canvas canvas) {
20511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            super.onDraw(canvas);
20611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
20711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            final Paint paint = mPaint;
20811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            final Rect bounds = mTempBounds;
20911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            final int height = getHeight();
21011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            final int width = getWidth();
21111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
21211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            for (CustomItem item : mItems) {
21346f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                if (item.mParent == null) {
21446f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                    paint.setColor(item.mChecked ? Color.RED : Color.BLUE);
21546f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                } else {
21646f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                    paint.setColor(item.mChecked ? Color.MAGENTA : Color.GREEN);
21746f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                }
21811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                paint.setStyle(Style.FILL);
2193b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                scaleRectF(item.mBoundsInRoot, bounds, width, height);
22011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                canvas.drawRect(bounds, paint);
22111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                paint.setColor(Color.WHITE);
22211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                paint.setTextAlign(Align.CENTER);
22346f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                canvas.drawText(item.mDescription, bounds.centerX(), bounds.centerY(), paint);
22411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
22511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
22611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
22711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        protected boolean onItemClicked(int index) {
22811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            final CustomItem item = getItem(index);
22911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            if (item == null) {
23011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                return false;
23111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
23211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
23346f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            item.mChecked = !item.mChecked;
23411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            invalidate();
23511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
23611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            // Since the item's checked state is exposed to accessibility
23711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            // services through its AccessibilityNodeInfo, we need to invalidate
23811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            // the item's virtual view. At some point in the future, the
23911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            // framework will obtain an updated version of the virtual view.
24011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            mTouchHelper.invalidateVirtualView(index);
24111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
24211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            // We also need to let the framework know what type of event
24311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            // happened. Accessibility services may use this event to provide
24411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            // appropriate feedback to the user.
24511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            mTouchHelper.sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);
24611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
24711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            return true;
24811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
24911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
25011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        protected int getItemIndexUnder(float x, float y) {
25111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            final float scaledX = (x / getWidth());
25211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            final float scaledY = (y / getHeight());
25311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            final int n = mItems.size();
25411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
25546f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            // Search in reverse order, so that topmost items are selected first.
25646f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            for (int i = n - 1; i >= 0; i--) {
25711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                final CustomItem item = mItems.get(i);
2583b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                if (item.mBoundsInRoot.contains(scaledX, scaledY)) {
25911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    return i;
26011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                }
26111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
26211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
26311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            return NO_ITEM;
26411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
26511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
26611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        protected CustomItem getItem(int index) {
26711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            if ((index < 0) || (index >= mItems.size())) {
26811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                return null;
26911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
27011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
27111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            return mItems.get(index);
27211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
27311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
27411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        protected static void scaleRectF(RectF in, Rect out, int width, int height) {
27511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            out.top = (int) (in.top * height);
27611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            out.bottom = (int) (in.bottom * height);
27711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            out.left = (int) (in.left * width);
27811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            out.right = (int) (in.right * width);
27911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
28011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
28111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        private class CustomViewTouchHelper extends ExploreByTouchHelper {
28211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            private final Rect mTempRect = new Rect();
28311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
28411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            public CustomViewTouchHelper(View forView) {
28511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                super(forView);
28611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
28711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
28811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            @Override
28911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            protected int getVirtualViewAt(float x, float y) {
29011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // We also perform hit detection in onTouchEvent(), and we can
29111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // reuse that logic here. This will ensure consistency whether
29211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // accessibility is on or off.
29311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                final int index = getItemIndexUnder(x, y);
29411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                if (index == NO_ITEM) {
29511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    return ExploreByTouchHelper.INVALID_ID;
29611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                }
29711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
29811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                return index;
29911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
30011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
30111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            @Override
30211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
30311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // Since every item should be visible, and since we're mapping
30446f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                // directly from item index to virtual view id, we can add
30546f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                // the index of every view that doesn't have a parent.
30611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                final int n = mItems.size();
30711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                for (int i = 0; i < n; i++) {
30846f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                    if (mItems.get(i).mParent == null) {
30946f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                        virtualViewIds.add(i);
31046f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                    }
31111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                }
31211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
31311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
31411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            @Override
31511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            protected void onPopulateEventForVirtualView(
31611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    int virtualViewId, AccessibilityEvent event) {
31711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                final CustomItem item = getItem(virtualViewId);
31811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                if (item == null) {
31911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    throw new IllegalArgumentException("Invalid virtual view id");
32011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                }
32111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
32211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // The event must be populated with text, either using
32311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // getText().add() or setContentDescription(). Since the item's
32411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // description is displayed visually, we'll add it to the event
32511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // text. If it was only used for accessibility, we would use
32611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // setContentDescription().
32746f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                event.getText().add(item.mDescription);
32811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
32911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
33011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            @Override
33111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            protected void onPopulateNodeForVirtualView(
33211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    int virtualViewId, AccessibilityNodeInfoCompat node) {
33311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                final CustomItem item = getItem(virtualViewId);
33411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                if (item == null) {
33511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    throw new IllegalArgumentException("Invalid virtual view id");
33611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                }
33711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
33811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // Node and event text and content descriptions are usually
33911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // identical, so we'll use the exact same string as before.
34046f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                node.setText(item.mDescription);
34111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
34211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // Reported bounds should be consistent with those used to draw
34311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // the item in onDraw(). They should also be consistent with the
34411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // hit detection performed in getVirtualViewAt() and
34511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // onTouchEvent().
34611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                final Rect bounds = mTempRect;
3473b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                int height = getHeight();
3483b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                int width = getWidth();
3493b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                if (item.mParent != null) {
3503b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                    width = (int) (width * item.mParent.mBoundsInRoot.width());
3513b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                    height = (int) (height * item.mParent.mBoundsInRoot.height());
3523b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia                }
35346f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                scaleRectF(item.mBounds, bounds, width, height);
35411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                node.setBoundsInParent(bounds);
35511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
35611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // Since the user can tap an item, add the CLICK action. We'll
35711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // need to handle this later in onPerformActionForVirtualView.
35811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
35911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
36011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                // This item has a checked state.
36111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                node.setCheckable(true);
36246f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                node.setChecked(item.mChecked);
36346f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia
36446f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                // Setup the hierarchy.
36546f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                if (item.mParent != null) {
36646f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                    node.setParent(CustomView.this, item.mParent.mId);
36746f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                }
36846f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                for (Integer id : item.mChildren) {
36946f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                    node.addChild(CustomView.this, id);
37046f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia                }
37111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
37211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
37311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            @Override
37411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            protected boolean onPerformActionForVirtualView(
37511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    int virtualViewId, int action, Bundle arguments) {
37611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                switch (action) {
37711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                    case AccessibilityNodeInfoCompat.ACTION_CLICK:
37811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                        // Click handling should be consistent with
37911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                        // onTouchEvent(). This ensures that the view works the
38011d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                        // same whether accessibility is turned on or off.
38111d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                        return onItemClicked(virtualViewId);
38211d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                }
38311d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
38411d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette                return false;
38511d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette            }
38611d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
38711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
38811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette
38911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        public static class CustomItem {
39046f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            private int mId;
39146f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            private CustomItem mParent;
39246f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            private List<Integer> mChildren = new ArrayList<>();
39346f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            private String mDescription;
39446f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            private RectF mBounds;
3953b591130c68223aa613660aaff79e65e26b5fc06Zach Kuznia            private RectF mBoundsInRoot;
39646f49c1900fc65a3e3bc0057e28161be6230e1daZach Kuznia            private boolean mChecked;
39711d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette        }
39811d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette    }
39911d03f96a3f29508c703b3df60a634eeaba5d37dAlan Viverette}
400