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