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.android.internal.widget; 18 19 import android.content.Context; 20 import android.graphics.Rect; 21 import android.os.Bundle; 22 import android.util.IntArray; 23 import android.view.MotionEvent; 24 import android.view.View; 25 import android.view.ViewParent; 26 import android.view.accessibility.AccessibilityEvent; 27 import android.view.accessibility.AccessibilityManager; 28 import android.view.accessibility.AccessibilityNodeInfo; 29 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 30 import android.view.accessibility.AccessibilityNodeProvider; 31 32 /** 33 * ExploreByTouchHelper is a utility class for implementing accessibility 34 * support in custom {@link android.view.View}s that represent a collection of View-like 35 * logical items. It extends {@link android.view.accessibility.AccessibilityNodeProvider} and 36 * simplifies many aspects of providing information to accessibility services 37 * and managing accessibility focus. This class does not currently support 38 * hierarchies of logical items. 39 * <p> 40 * This should be applied to the parent view using 41 * {@link android.view.View#setAccessibilityDelegate}: 42 * 43 * <pre> 44 * mAccessHelper = ExploreByTouchHelper.create(someView, mAccessHelperCallback); 45 * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper); 46 * </pre> 47 */ 48 public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { 49 /** Virtual node identifier value for invalid nodes. */ 50 public static final int INVALID_ID = Integer.MIN_VALUE; 51 52 /** Virtual node identifier value for the host view's node. */ 53 public static final int HOST_ID = View.NO_ID; 54 55 /** Default class name used for virtual views. */ 56 private static final String DEFAULT_CLASS_NAME = View.class.getName(); 57 58 /** Default bounds used to determine if the client didn't set any. */ 59 private static final Rect INVALID_PARENT_BOUNDS = new Rect( 60 Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); 61 62 // Lazily-created temporary data structures used when creating nodes. 63 private Rect mTempScreenRect; 64 private Rect mTempParentRect; 65 private int[] mTempGlobalRect; 66 67 /** Lazily-created temporary data structure used to compute visibility. */ 68 private Rect mTempVisibleRect; 69 70 /** Lazily-created temporary data structure used to obtain child IDs. */ 71 private IntArray mTempArray; 72 73 /** System accessibility manager, used to check state and send events. */ 74 private final AccessibilityManager mManager; 75 76 /** View whose internal structure is exposed through this helper. */ 77 private final View mView; 78 79 /** Context of the host view. **/ 80 private final Context mContext; 81 82 /** Node provider that handles creating nodes and performing actions. */ 83 private ExploreByTouchNodeProvider mNodeProvider; 84 85 /** Virtual view id for the currently focused logical item. */ 86 private int mFocusedVirtualViewId = INVALID_ID; 87 88 /** Virtual view id for the currently hovered logical item. */ 89 private int mHoveredVirtualViewId = INVALID_ID; 90 91 /** 92 * Factory method to create a new {@link ExploreByTouchHelper}. 93 * 94 * @param forView View whose logical children are exposed by this helper. 95 */ ExploreByTouchHelper(View forView)96 public ExploreByTouchHelper(View forView) { 97 if (forView == null) { 98 throw new IllegalArgumentException("View may not be null"); 99 } 100 101 mView = forView; 102 mContext = forView.getContext(); 103 mManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 104 } 105 106 /** 107 * Returns the {@link android.view.accessibility.AccessibilityNodeProvider} for this helper. 108 * 109 * @param host View whose logical children are exposed by this helper. 110 * @return The accessibility node provider for this helper. 111 */ 112 @Override getAccessibilityNodeProvider(View host)113 public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) { 114 if (mNodeProvider == null) { 115 mNodeProvider = new ExploreByTouchNodeProvider(); 116 } 117 return mNodeProvider; 118 } 119 120 /** 121 * Dispatches hover {@link android.view.MotionEvent}s to the virtual view hierarchy when 122 * the Explore by Touch feature is enabled. 123 * <p> 124 * This method should be called by overriding 125 * {@link View#dispatchHoverEvent}: 126 * 127 * <pre>@Override 128 * public boolean dispatchHoverEvent(MotionEvent event) { 129 * if (mHelper.dispatchHoverEvent(this, event) { 130 * return true; 131 * } 132 * return super.dispatchHoverEvent(event); 133 * } 134 * </pre> 135 * 136 * @param event The hover event to dispatch to the virtual view hierarchy. 137 * @return Whether the hover event was handled. 138 */ dispatchHoverEvent(MotionEvent event)139 public boolean dispatchHoverEvent(MotionEvent event) { 140 if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) { 141 return false; 142 } 143 144 switch (event.getAction()) { 145 case MotionEvent.ACTION_HOVER_MOVE: 146 case MotionEvent.ACTION_HOVER_ENTER: 147 final int virtualViewId = getVirtualViewAt(event.getX(), event.getY()); 148 updateHoveredVirtualView(virtualViewId); 149 return (virtualViewId != INVALID_ID); 150 case MotionEvent.ACTION_HOVER_EXIT: 151 if (mHoveredVirtualViewId != INVALID_ID) { 152 updateHoveredVirtualView(INVALID_ID); 153 return true; 154 } 155 return false; 156 default: 157 return false; 158 } 159 } 160 161 /** 162 * Populates an event of the specified type with information about an item 163 * and attempts to send it up through the view hierarchy. 164 * <p> 165 * You should call this method after performing a user action that normally 166 * fires an accessibility event, such as clicking on an item. 167 * 168 * <pre>public void performItemClick(T item) { 169 * ... 170 * sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED); 171 * } 172 * </pre> 173 * 174 * @param virtualViewId The virtual view id for which to send an event. 175 * @param eventType The type of event to send. 176 * @return true if the event was sent successfully. 177 */ sendEventForVirtualView(int virtualViewId, int eventType)178 public boolean sendEventForVirtualView(int virtualViewId, int eventType) { 179 if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) { 180 return false; 181 } 182 183 final ViewParent parent = mView.getParent(); 184 if (parent == null) { 185 return false; 186 } 187 188 final AccessibilityEvent event = createEvent(virtualViewId, eventType); 189 return parent.requestSendAccessibilityEvent(mView, event); 190 } 191 192 /** 193 * Notifies the accessibility framework that the properties of the parent 194 * view have changed. 195 * <p> 196 * You <b>must</b> call this method after adding or removing items from the 197 * parent view. 198 */ invalidateRoot()199 public void invalidateRoot() { 200 invalidateVirtualView(HOST_ID, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); 201 } 202 203 /** 204 * Notifies the accessibility framework that the properties of a particular 205 * item have changed. 206 * <p> 207 * You <b>must</b> call this method after changing any of the properties set 208 * in {@link #onPopulateNodeForVirtualView}. 209 * 210 * @param virtualViewId The virtual view id to invalidate, or 211 * {@link #HOST_ID} to invalidate the root view. 212 * @see #invalidateVirtualView(int, int) 213 */ invalidateVirtualView(int virtualViewId)214 public void invalidateVirtualView(int virtualViewId) { 215 invalidateVirtualView(virtualViewId, 216 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); 217 } 218 219 /** 220 * Notifies the accessibility framework that the properties of a particular 221 * item have changed. 222 * <p> 223 * You <b>must</b> call this method after changing any of the properties set 224 * in {@link #onPopulateNodeForVirtualView}. 225 * 226 * @param virtualViewId The virtual view id to invalidate, or 227 * {@link #HOST_ID} to invalidate the root view. 228 * @param changeTypes The bit mask of change types. May be {@code 0} for the 229 * default (undefined) change type or one or more of: 230 * <ul> 231 * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} 232 * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_STATE_DESCRIPTION} 233 * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE} 234 * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT} 235 * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED} 236 * </ul> 237 */ invalidateVirtualView(int virtualViewId, int changeTypes)238 public void invalidateVirtualView(int virtualViewId, int changeTypes) { 239 if (virtualViewId != INVALID_ID && mManager.isEnabled()) { 240 final ViewParent parent = mView.getParent(); 241 if (parent != null) { 242 final AccessibilityEvent event = createEvent(virtualViewId, 243 AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 244 event.setContentChangeTypes(changeTypes); 245 parent.requestSendAccessibilityEvent(mView, event); 246 } 247 } 248 } 249 250 /** 251 * Returns the virtual view id for the currently focused item, 252 * 253 * @return A virtual view id, or {@link #INVALID_ID} if no item is 254 * currently focused. 255 */ getFocusedVirtualView()256 public int getFocusedVirtualView() { 257 return mFocusedVirtualViewId; 258 } 259 260 /** 261 * Sets the currently hovered item, sending hover accessibility events as 262 * necessary to maintain the correct state. 263 * 264 * @param virtualViewId The virtual view id for the item currently being 265 * hovered, or {@link #INVALID_ID} if no item is hovered within 266 * the parent view. 267 */ updateHoveredVirtualView(int virtualViewId)268 private void updateHoveredVirtualView(int virtualViewId) { 269 if (mHoveredVirtualViewId == virtualViewId) { 270 return; 271 } 272 273 final int previousVirtualViewId = mHoveredVirtualViewId; 274 mHoveredVirtualViewId = virtualViewId; 275 276 // Stay consistent with framework behavior by sending ENTER/EXIT pairs 277 // in reverse order. This is accurate as of API 18. 278 sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 279 sendEventForVirtualView(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 280 } 281 282 /** 283 * Constructs and returns an {@link AccessibilityEvent} for the specified 284 * virtual view id, which includes the host view ({@link #HOST_ID}). 285 * 286 * @param virtualViewId The virtual view id for the item for which to 287 * construct an event. 288 * @param eventType The type of event to construct. 289 * @return An {@link AccessibilityEvent} populated with information about 290 * the specified item. 291 */ createEvent(int virtualViewId, int eventType)292 private AccessibilityEvent createEvent(int virtualViewId, int eventType) { 293 switch (virtualViewId) { 294 case HOST_ID: 295 return createEventForHost(eventType); 296 default: 297 return createEventForChild(virtualViewId, eventType); 298 } 299 } 300 301 /** 302 * Constructs and returns an {@link AccessibilityEvent} for the host node. 303 * 304 * @param eventType The type of event to construct. 305 * @return An {@link AccessibilityEvent} populated with information about 306 * the specified item. 307 */ createEventForHost(int eventType)308 private AccessibilityEvent createEventForHost(int eventType) { 309 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 310 mView.onInitializeAccessibilityEvent(event); 311 312 // Allow the client to populate the event. 313 onPopulateEventForHost(event); 314 315 return event; 316 } 317 318 /** 319 * Constructs and returns an {@link AccessibilityEvent} populated with 320 * information about the specified item. 321 * 322 * @param virtualViewId The virtual view id for the item for which to 323 * construct an event. 324 * @param eventType The type of event to construct. 325 * @return An {@link AccessibilityEvent} populated with information about 326 * the specified item. 327 */ createEventForChild(int virtualViewId, int eventType)328 private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) { 329 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 330 event.setEnabled(true); 331 event.setClassName(DEFAULT_CLASS_NAME); 332 333 // Allow the client to populate the event. 334 onPopulateEventForVirtualView(virtualViewId, event); 335 336 // Make sure the developer is following the rules. 337 if (event.getText().isEmpty() && (event.getContentDescription() == null)) { 338 throw new RuntimeException("Callbacks must add text or a content description in " 339 + "populateEventForVirtualViewId()"); 340 } 341 342 // Don't allow the client to override these properties. 343 event.setPackageName(mView.getContext().getPackageName()); 344 event.setSource(mView, virtualViewId); 345 346 return event; 347 } 348 349 /** 350 * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the 351 * specified virtual view id, which includes the host view 352 * ({@link #HOST_ID}). 353 * 354 * @param virtualViewId The virtual view id for the item for which to 355 * construct a node. 356 * @return An {@link android.view.accessibility.AccessibilityNodeInfo} populated with information 357 * about the specified item. 358 */ createNode(int virtualViewId)359 private AccessibilityNodeInfo createNode(int virtualViewId) { 360 switch (virtualViewId) { 361 case HOST_ID: 362 return createNodeForHost(); 363 default: 364 return createNodeForChild(virtualViewId); 365 } 366 } 367 368 /** 369 * Constructs and returns an {@link AccessibilityNodeInfo} for the 370 * host view populated with its virtual descendants. 371 * 372 * @return An {@link AccessibilityNodeInfo} for the parent node. 373 */ createNodeForHost()374 private AccessibilityNodeInfo createNodeForHost() { 375 final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView); 376 mView.onInitializeAccessibilityNodeInfo(node); 377 final int realNodeCount = node.getChildCount(); 378 379 // Allow the client to populate the host node. 380 onPopulateNodeForHost(node); 381 382 // Add the virtual descendants. 383 if (mTempArray == null) { 384 mTempArray = new IntArray(); 385 } else { 386 mTempArray.clear(); 387 } 388 final IntArray virtualViewIds = mTempArray; 389 getVisibleVirtualViews(virtualViewIds); 390 if (realNodeCount > 0 && virtualViewIds.size() > 0) { 391 throw new RuntimeException("Views cannot have both real and virtual children"); 392 } 393 394 final int N = virtualViewIds.size(); 395 for (int i = 0; i < N; i++) { 396 node.addChild(mView, virtualViewIds.get(i)); 397 } 398 399 return node; 400 } 401 402 /** 403 * Constructs and returns an {@link AccessibilityNodeInfo} for the 404 * specified item. Automatically manages accessibility focus actions. 405 * <p> 406 * Allows the implementing class to specify most node properties, but 407 * overrides the following: 408 * <ul> 409 * <li>{@link AccessibilityNodeInfo#setPackageName} 410 * <li>{@link AccessibilityNodeInfo#setClassName} 411 * <li>{@link AccessibilityNodeInfo#setParent(View)} 412 * <li>{@link AccessibilityNodeInfo#setSource(View, int)} 413 * <li>{@link AccessibilityNodeInfo#setVisibleToUser} 414 * <li>{@link AccessibilityNodeInfo#setBoundsInScreen(Rect)} 415 * </ul> 416 * <p> 417 * Uses the bounds of the parent view and the parent-relative bounding 418 * rectangle specified by 419 * {@link AccessibilityNodeInfo#getBoundsInParent} to automatically 420 * update the following properties: 421 * <ul> 422 * <li>{@link AccessibilityNodeInfo#setVisibleToUser} 423 * <li>{@link AccessibilityNodeInfo#setBoundsInParent} 424 * </ul> 425 * 426 * @param virtualViewId The virtual view id for item for which to construct 427 * a node. 428 * @return An {@link AccessibilityNodeInfo} for the specified item. 429 */ createNodeForChild(int virtualViewId)430 private AccessibilityNodeInfo createNodeForChild(int virtualViewId) { 431 ensureTempRects(); 432 final Rect tempParentRect = mTempParentRect; 433 final int[] tempGlobalRect = mTempGlobalRect; 434 final Rect tempScreenRect = mTempScreenRect; 435 436 final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(); 437 438 // Ensure the client has good defaults. 439 node.setEnabled(true); 440 node.setClassName(DEFAULT_CLASS_NAME); 441 node.setBoundsInParent(INVALID_PARENT_BOUNDS); 442 443 // Allow the client to populate the node. 444 onPopulateNodeForVirtualView(virtualViewId, node); 445 446 // Make sure the developer is following the rules. 447 if ((node.getText() == null) && (node.getContentDescription() == null)) { 448 throw new RuntimeException("Callbacks must add text or a content description in " 449 + "populateNodeForVirtualViewId()"); 450 } 451 452 node.getBoundsInParent(tempParentRect); 453 if (tempParentRect.equals(INVALID_PARENT_BOUNDS)) { 454 throw new RuntimeException("Callbacks must set parent bounds in " 455 + "populateNodeForVirtualViewId()"); 456 } 457 458 final int actions = node.getActions(); 459 if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) { 460 throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in " 461 + "populateNodeForVirtualViewId()"); 462 } 463 if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) { 464 throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in " 465 + "populateNodeForVirtualViewId()"); 466 } 467 468 // Don't allow the client to override these properties. 469 node.setPackageName(mView.getContext().getPackageName()); 470 node.setSource(mView, virtualViewId); 471 node.setParent(mView); 472 473 // Manage internal accessibility focus state. 474 if (mFocusedVirtualViewId == virtualViewId) { 475 node.setAccessibilityFocused(true); 476 node.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 477 } else { 478 node.setAccessibilityFocused(false); 479 node.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); 480 } 481 482 // Set the visibility based on the parent bound. 483 if (intersectVisibleToUser(tempParentRect)) { 484 node.setVisibleToUser(true); 485 node.setBoundsInParent(tempParentRect); 486 } 487 488 // Calculate screen-relative bound. 489 mView.getLocationOnScreen(tempGlobalRect); 490 final int offsetX = tempGlobalRect[0]; 491 final int offsetY = tempGlobalRect[1]; 492 tempScreenRect.set(tempParentRect); 493 tempScreenRect.offset(offsetX, offsetY); 494 node.setBoundsInScreen(tempScreenRect); 495 496 return node; 497 } 498 ensureTempRects()499 private void ensureTempRects() { 500 mTempGlobalRect = new int[2]; 501 mTempParentRect = new Rect(); 502 mTempScreenRect = new Rect(); 503 } 504 performAction(int virtualViewId, int action, Bundle arguments)505 private boolean performAction(int virtualViewId, int action, Bundle arguments) { 506 switch (virtualViewId) { 507 case HOST_ID: 508 return performActionForHost(action, arguments); 509 default: 510 return performActionForChild(virtualViewId, action, arguments); 511 } 512 } 513 performActionForHost(int action, Bundle arguments)514 private boolean performActionForHost(int action, Bundle arguments) { 515 return mView.performAccessibilityAction(action, arguments); 516 } 517 performActionForChild(int virtualViewId, int action, Bundle arguments)518 private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) { 519 switch (action) { 520 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: 521 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: 522 return manageFocusForChild(virtualViewId, action); 523 default: 524 return onPerformActionForVirtualView(virtualViewId, action, arguments); 525 } 526 } 527 manageFocusForChild(int virtualViewId, int action)528 private boolean manageFocusForChild(int virtualViewId, int action) { 529 switch (action) { 530 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: 531 return requestAccessibilityFocus(virtualViewId); 532 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: 533 return clearAccessibilityFocus(virtualViewId); 534 default: 535 return false; 536 } 537 } 538 539 /** 540 * Computes whether the specified {@link Rect} intersects with the visible 541 * portion of its parent {@link View}. Modifies {@code localRect} to contain 542 * only the visible portion. 543 * 544 * @param localRect A rectangle in local (parent) coordinates. 545 * @return Whether the specified {@link Rect} is visible on the screen. 546 */ intersectVisibleToUser(Rect localRect)547 private boolean intersectVisibleToUser(Rect localRect) { 548 // Missing or empty bounds mean this view is not visible. 549 if ((localRect == null) || localRect.isEmpty()) { 550 return false; 551 } 552 553 // Attached to invisible window means this view is not visible. 554 if (mView.getWindowVisibility() != View.VISIBLE) { 555 return false; 556 } 557 558 // An invisible predecessor means that this view is not visible. 559 ViewParent viewParent = mView.getParent(); 560 while (viewParent instanceof View) { 561 final View view = (View) viewParent; 562 if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) { 563 return false; 564 } 565 viewParent = view.getParent(); 566 } 567 568 // A null parent implies the view is not visible. 569 if (viewParent == null) { 570 return false; 571 } 572 573 // If no portion of the parent is visible, this view is not visible. 574 if (mTempVisibleRect == null) { 575 mTempVisibleRect = new Rect(); 576 } 577 final Rect tempVisibleRect = mTempVisibleRect; 578 if (!mView.getLocalVisibleRect(tempVisibleRect)) { 579 return false; 580 } 581 582 // Check if the view intersects the visible portion of the parent. 583 return localRect.intersect(tempVisibleRect); 584 } 585 586 /** 587 * Returns whether this virtual view is accessibility focused. 588 * 589 * @return True if the view is accessibility focused. 590 */ isAccessibilityFocused(int virtualViewId)591 private boolean isAccessibilityFocused(int virtualViewId) { 592 return (mFocusedVirtualViewId == virtualViewId); 593 } 594 595 /** 596 * Attempts to give accessibility focus to a virtual view. 597 * <p> 598 * A virtual view will not actually take focus if 599 * {@link AccessibilityManager#isEnabled()} returns false, 600 * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false, 601 * or the view already has accessibility focus. 602 * 603 * @param virtualViewId The id of the virtual view on which to place 604 * accessibility focus. 605 * @return Whether this virtual view actually took accessibility focus. 606 */ requestAccessibilityFocus(int virtualViewId)607 private boolean requestAccessibilityFocus(int virtualViewId) { 608 final AccessibilityManager accessibilityManager = 609 (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 610 611 if (!mManager.isEnabled() 612 || !accessibilityManager.isTouchExplorationEnabled()) { 613 return false; 614 } 615 // TODO: Check virtual view visibility. 616 if (!isAccessibilityFocused(virtualViewId)) { 617 // Clear focus from the previously focused view, if applicable. 618 if (mFocusedVirtualViewId != INVALID_ID) { 619 sendEventForVirtualView(mFocusedVirtualViewId, 620 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 621 } 622 623 // Set focus on the new view. 624 mFocusedVirtualViewId = virtualViewId; 625 626 // TODO: Only invalidate virtual view bounds. 627 mView.invalidate(); 628 sendEventForVirtualView(virtualViewId, 629 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 630 return true; 631 } 632 return false; 633 } 634 635 /** 636 * Attempts to clear accessibility focus from a virtual view. 637 * 638 * @param virtualViewId The id of the virtual view from which to clear 639 * accessibility focus. 640 * @return Whether this virtual view actually cleared accessibility focus. 641 */ clearAccessibilityFocus(int virtualViewId)642 private boolean clearAccessibilityFocus(int virtualViewId) { 643 if (isAccessibilityFocused(virtualViewId)) { 644 mFocusedVirtualViewId = INVALID_ID; 645 mView.invalidate(); 646 sendEventForVirtualView(virtualViewId, 647 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 648 return true; 649 } 650 return false; 651 } 652 653 /** 654 * Provides a mapping between view-relative coordinates and logical 655 * items. 656 * 657 * @param x The view-relative x coordinate 658 * @param y The view-relative y coordinate 659 * @return virtual view identifier for the logical item under 660 * coordinates (x,y) 661 */ getVirtualViewAt(float x, float y)662 protected abstract int getVirtualViewAt(float x, float y); 663 664 /** 665 * Populates a list with the view's visible items. The ordering of items 666 * within {@code virtualViewIds} specifies order of accessibility focus 667 * traversal. 668 * 669 * @param virtualViewIds The list to populate with visible items 670 */ getVisibleVirtualViews(IntArray virtualViewIds)671 protected abstract void getVisibleVirtualViews(IntArray virtualViewIds); 672 673 /** 674 * Populates an {@link AccessibilityEvent} with information about the 675 * specified item. 676 * <p> 677 * Implementations <b>must</b> populate the following required fields: 678 * <ul> 679 * <li>event text, see {@link AccessibilityEvent#getText} or 680 * {@link AccessibilityEvent#setContentDescription} 681 * </ul> 682 * <p> 683 * The helper class automatically populates the following fields with 684 * default values, but implementations may optionally override them: 685 * <ul> 686 * <li>item class name, set to android.view.View, see 687 * {@link AccessibilityEvent#setClassName} 688 * </ul> 689 * <p> 690 * The following required fields are automatically populated by the 691 * helper class and may not be overridden: 692 * <ul> 693 * <li>package name, set to the package of the host view's 694 * {@link Context}, see {@link AccessibilityEvent#setPackageName} 695 * <li>event source, set to the host view and virtual view identifier, 696 * see {@link android.view.accessibility.AccessibilityRecord#setSource(View, int)} 697 * </ul> 698 * 699 * @param virtualViewId The virtual view id for the item for which to 700 * populate the event 701 * @param event The event to populate 702 */ onPopulateEventForVirtualView( int virtualViewId, AccessibilityEvent event)703 protected abstract void onPopulateEventForVirtualView( 704 int virtualViewId, AccessibilityEvent event); 705 706 /** 707 * Populates an {@link AccessibilityEvent} with information about the host 708 * view. 709 * <p> 710 * The default implementation is a no-op. 711 * 712 * @param event the event to populate with information about the host view 713 */ onPopulateEventForHost(AccessibilityEvent event)714 protected void onPopulateEventForHost(AccessibilityEvent event) { 715 // Default implementation is no-op. 716 } 717 718 /** 719 * Populates an {@link AccessibilityNodeInfo} with information 720 * about the specified item. 721 * <p> 722 * Implementations <b>must</b> populate the following required fields: 723 * <ul> 724 * <li>event text, see {@link AccessibilityNodeInfo#setText} or 725 * {@link AccessibilityNodeInfo#setContentDescription} 726 * <li>bounds in parent coordinates, see 727 * {@link AccessibilityNodeInfo#setBoundsInParent} 728 * </ul> 729 * <p> 730 * The helper class automatically populates the following fields with 731 * default values, but implementations may optionally override them: 732 * <ul> 733 * <li>enabled state, set to true, see 734 * {@link AccessibilityNodeInfo#setEnabled} 735 * <li>item class name, identical to the class name set by 736 * {@link #onPopulateEventForVirtualView}, see 737 * {@link AccessibilityNodeInfo#setClassName} 738 * </ul> 739 * <p> 740 * The following required fields are automatically populated by the 741 * helper class and may not be overridden: 742 * <ul> 743 * <li>package name, identical to the package name set by 744 * {@link #onPopulateEventForVirtualView}, see 745 * {@link AccessibilityNodeInfo#setPackageName} 746 * <li>node source, identical to the event source set in 747 * {@link #onPopulateEventForVirtualView}, see 748 * {@link AccessibilityNodeInfo#setSource(View, int)} 749 * <li>parent view, set to the host view, see 750 * {@link AccessibilityNodeInfo#setParent(View)} 751 * <li>visibility, computed based on parent-relative bounds, see 752 * {@link AccessibilityNodeInfo#setVisibleToUser} 753 * <li>accessibility focus, computed based on internal helper state, see 754 * {@link AccessibilityNodeInfo#setAccessibilityFocused} 755 * <li>bounds in screen coordinates, computed based on host view bounds, 756 * see {@link AccessibilityNodeInfo#setBoundsInScreen} 757 * </ul> 758 * <p> 759 * Additionally, the helper class automatically handles accessibility 760 * focus management by adding the appropriate 761 * {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} or 762 * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS} 763 * action. Implementations must <b>never</b> manually add these actions. 764 * <p> 765 * The helper class also automatically modifies parent- and 766 * screen-relative bounds to reflect the portion of the item visible 767 * within its parent. 768 * 769 * @param virtualViewId The virtual view identifier of the item for 770 * which to populate the node 771 * @param node The node to populate 772 */ onPopulateNodeForVirtualView( int virtualViewId, AccessibilityNodeInfo node)773 protected abstract void onPopulateNodeForVirtualView( 774 int virtualViewId, AccessibilityNodeInfo node); 775 776 /** 777 * Populates an {@link AccessibilityNodeInfo} with information about the 778 * host view. 779 * <p> 780 * The default implementation is a no-op. 781 * 782 * @param node the node to populate with information about the host view 783 */ onPopulateNodeForHost(AccessibilityNodeInfo node)784 protected void onPopulateNodeForHost(AccessibilityNodeInfo node) { 785 // Default implementation is no-op. 786 } 787 788 /** 789 * Performs the specified accessibility action on the item associated 790 * with the virtual view identifier. See 791 * {@link AccessibilityNodeInfo#performAction(int, Bundle)} for 792 * more information. 793 * <p> 794 * Implementations <b>must</b> handle any actions added manually in 795 * {@link #onPopulateNodeForVirtualView}. 796 * <p> 797 * The helper class automatically handles focus management resulting 798 * from {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} 799 * and 800 * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS} 801 * actions. 802 * 803 * @param virtualViewId The virtual view identifier of the item on which 804 * to perform the action 805 * @param action The accessibility action to perform 806 * @param arguments (Optional) A bundle with additional arguments, or 807 * null 808 * @return true if the action was performed 809 */ onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments)810 protected abstract boolean onPerformActionForVirtualView( 811 int virtualViewId, int action, Bundle arguments); 812 813 /** 814 * Exposes a virtual view hierarchy to the accessibility framework. Only 815 * used in API 16+. 816 */ 817 private class ExploreByTouchNodeProvider extends AccessibilityNodeProvider { 818 @Override createAccessibilityNodeInfo(int virtualViewId)819 public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { 820 return ExploreByTouchHelper.this.createNode(virtualViewId); 821 } 822 823 @Override performAction(int virtualViewId, int action, Bundle arguments)824 public boolean performAction(int virtualViewId, int action, Bundle arguments) { 825 return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments); 826 } 827 } 828 } 829