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