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