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_SUBTREE} 233 * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT} 234 * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED} 235 * </ul> 236 */ invalidateVirtualView(int virtualViewId, int changeTypes)237 public void invalidateVirtualView(int virtualViewId, int changeTypes) { 238 if (virtualViewId != INVALID_ID && mManager.isEnabled()) { 239 final ViewParent parent = mView.getParent(); 240 if (parent != null) { 241 final AccessibilityEvent event = createEvent(virtualViewId, 242 AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 243 event.setContentChangeTypes(changeTypes); 244 parent.requestSendAccessibilityEvent(mView, event); 245 } 246 } 247 } 248 249 /** 250 * Returns the virtual view id for the currently focused item, 251 * 252 * @return A virtual view id, or {@link #INVALID_ID} if no item is 253 * currently focused. 254 */ getFocusedVirtualView()255 public int getFocusedVirtualView() { 256 return mFocusedVirtualViewId; 257 } 258 259 /** 260 * Sets the currently hovered item, sending hover accessibility events as 261 * necessary to maintain the correct state. 262 * 263 * @param virtualViewId The virtual view id for the item currently being 264 * hovered, or {@link #INVALID_ID} if no item is hovered within 265 * the parent view. 266 */ updateHoveredVirtualView(int virtualViewId)267 private void updateHoveredVirtualView(int virtualViewId) { 268 if (mHoveredVirtualViewId == virtualViewId) { 269 return; 270 } 271 272 final int previousVirtualViewId = mHoveredVirtualViewId; 273 mHoveredVirtualViewId = virtualViewId; 274 275 // Stay consistent with framework behavior by sending ENTER/EXIT pairs 276 // in reverse order. This is accurate as of API 18. 277 sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 278 sendEventForVirtualView(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 279 } 280 281 /** 282 * Constructs and returns an {@link AccessibilityEvent} for the specified 283 * virtual view id, which includes the host view ({@link #HOST_ID}). 284 * 285 * @param virtualViewId The virtual view id for the item for which to 286 * construct an event. 287 * @param eventType The type of event to construct. 288 * @return An {@link AccessibilityEvent} populated with information about 289 * the specified item. 290 */ createEvent(int virtualViewId, int eventType)291 private AccessibilityEvent createEvent(int virtualViewId, int eventType) { 292 switch (virtualViewId) { 293 case HOST_ID: 294 return createEventForHost(eventType); 295 default: 296 return createEventForChild(virtualViewId, eventType); 297 } 298 } 299 300 /** 301 * Constructs and returns an {@link AccessibilityEvent} for the host node. 302 * 303 * @param eventType The type of event to construct. 304 * @return An {@link AccessibilityEvent} populated with information about 305 * the specified item. 306 */ createEventForHost(int eventType)307 private AccessibilityEvent createEventForHost(int eventType) { 308 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 309 mView.onInitializeAccessibilityEvent(event); 310 311 // Allow the client to populate the event. 312 onPopulateEventForHost(event); 313 314 return event; 315 } 316 317 /** 318 * Constructs and returns an {@link AccessibilityEvent} populated with 319 * information about the specified item. 320 * 321 * @param virtualViewId The virtual view id for the item for which to 322 * construct an event. 323 * @param eventType The type of event to construct. 324 * @return An {@link AccessibilityEvent} populated with information about 325 * the specified item. 326 */ createEventForChild(int virtualViewId, int eventType)327 private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) { 328 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 329 event.setEnabled(true); 330 event.setClassName(DEFAULT_CLASS_NAME); 331 332 // Allow the client to populate the event. 333 onPopulateEventForVirtualView(virtualViewId, event); 334 335 // Make sure the developer is following the rules. 336 if (event.getText().isEmpty() && (event.getContentDescription() == null)) { 337 throw new RuntimeException("Callbacks must add text or a content description in " 338 + "populateEventForVirtualViewId()"); 339 } 340 341 // Don't allow the client to override these properties. 342 event.setPackageName(mView.getContext().getPackageName()); 343 event.setSource(mView, virtualViewId); 344 345 return event; 346 } 347 348 /** 349 * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the 350 * specified virtual view id, which includes the host view 351 * ({@link #HOST_ID}). 352 * 353 * @param virtualViewId The virtual view id for the item for which to 354 * construct a node. 355 * @return An {@link android.view.accessibility.AccessibilityNodeInfo} populated with information 356 * about the specified item. 357 */ createNode(int virtualViewId)358 private AccessibilityNodeInfo createNode(int virtualViewId) { 359 switch (virtualViewId) { 360 case HOST_ID: 361 return createNodeForHost(); 362 default: 363 return createNodeForChild(virtualViewId); 364 } 365 } 366 367 /** 368 * Constructs and returns an {@link AccessibilityNodeInfo} for the 369 * host view populated with its virtual descendants. 370 * 371 * @return An {@link AccessibilityNodeInfo} for the parent node. 372 */ createNodeForHost()373 private AccessibilityNodeInfo createNodeForHost() { 374 final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView); 375 mView.onInitializeAccessibilityNodeInfo(node); 376 final int realNodeCount = node.getChildCount(); 377 378 // Allow the client to populate the host node. 379 onPopulateNodeForHost(node); 380 381 // Add the virtual descendants. 382 if (mTempArray == null) { 383 mTempArray = new IntArray(); 384 } else { 385 mTempArray.clear(); 386 } 387 final IntArray virtualViewIds = mTempArray; 388 getVisibleVirtualViews(virtualViewIds); 389 if (realNodeCount > 0 && virtualViewIds.size() > 0) { 390 throw new RuntimeException("Views cannot have both real and virtual children"); 391 } 392 393 final int N = virtualViewIds.size(); 394 for (int i = 0; i < N; i++) { 395 node.addChild(mView, virtualViewIds.get(i)); 396 } 397 398 return node; 399 } 400 401 /** 402 * Constructs and returns an {@link AccessibilityNodeInfo} for the 403 * specified item. Automatically manages accessibility focus actions. 404 * <p> 405 * Allows the implementing class to specify most node properties, but 406 * overrides the following: 407 * <ul> 408 * <li>{@link AccessibilityNodeInfo#setPackageName} 409 * <li>{@link AccessibilityNodeInfo#setClassName} 410 * <li>{@link AccessibilityNodeInfo#setParent(View)} 411 * <li>{@link AccessibilityNodeInfo#setSource(View, int)} 412 * <li>{@link AccessibilityNodeInfo#setVisibleToUser} 413 * <li>{@link AccessibilityNodeInfo#setBoundsInScreen(Rect)} 414 * </ul> 415 * <p> 416 * Uses the bounds of the parent view and the parent-relative bounding 417 * rectangle specified by 418 * {@link AccessibilityNodeInfo#getBoundsInParent} to automatically 419 * update the following properties: 420 * <ul> 421 * <li>{@link AccessibilityNodeInfo#setVisibleToUser} 422 * <li>{@link AccessibilityNodeInfo#setBoundsInParent} 423 * </ul> 424 * 425 * @param virtualViewId The virtual view id for item for which to construct 426 * a node. 427 * @return An {@link AccessibilityNodeInfo} for the specified item. 428 */ createNodeForChild(int virtualViewId)429 private AccessibilityNodeInfo createNodeForChild(int virtualViewId) { 430 ensureTempRects(); 431 final Rect tempParentRect = mTempParentRect; 432 final int[] tempGlobalRect = mTempGlobalRect; 433 final Rect tempScreenRect = mTempScreenRect; 434 435 final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(); 436 437 // Ensure the client has good defaults. 438 node.setEnabled(true); 439 node.setClassName(DEFAULT_CLASS_NAME); 440 node.setBoundsInParent(INVALID_PARENT_BOUNDS); 441 442 // Allow the client to populate the node. 443 onPopulateNodeForVirtualView(virtualViewId, node); 444 445 // Make sure the developer is following the rules. 446 if ((node.getText() == null) && (node.getContentDescription() == null)) { 447 throw new RuntimeException("Callbacks must add text or a content description in " 448 + "populateNodeForVirtualViewId()"); 449 } 450 451 node.getBoundsInParent(tempParentRect); 452 if (tempParentRect.equals(INVALID_PARENT_BOUNDS)) { 453 throw new RuntimeException("Callbacks must set parent bounds in " 454 + "populateNodeForVirtualViewId()"); 455 } 456 457 final int actions = node.getActions(); 458 if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) { 459 throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in " 460 + "populateNodeForVirtualViewId()"); 461 } 462 if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) { 463 throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in " 464 + "populateNodeForVirtualViewId()"); 465 } 466 467 // Don't allow the client to override these properties. 468 node.setPackageName(mView.getContext().getPackageName()); 469 node.setSource(mView, virtualViewId); 470 node.setParent(mView); 471 472 // Manage internal accessibility focus state. 473 if (mFocusedVirtualViewId == virtualViewId) { 474 node.setAccessibilityFocused(true); 475 node.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 476 } else { 477 node.setAccessibilityFocused(false); 478 node.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); 479 } 480 481 // Set the visibility based on the parent bound. 482 if (intersectVisibleToUser(tempParentRect)) { 483 node.setVisibleToUser(true); 484 node.setBoundsInParent(tempParentRect); 485 } 486 487 // Calculate screen-relative bound. 488 mView.getLocationOnScreen(tempGlobalRect); 489 final int offsetX = tempGlobalRect[0]; 490 final int offsetY = tempGlobalRect[1]; 491 tempScreenRect.set(tempParentRect); 492 tempScreenRect.offset(offsetX, offsetY); 493 node.setBoundsInScreen(tempScreenRect); 494 495 return node; 496 } 497 ensureTempRects()498 private void ensureTempRects() { 499 mTempGlobalRect = new int[2]; 500 mTempParentRect = new Rect(); 501 mTempScreenRect = new Rect(); 502 } 503 performAction(int virtualViewId, int action, Bundle arguments)504 private boolean performAction(int virtualViewId, int action, Bundle arguments) { 505 switch (virtualViewId) { 506 case HOST_ID: 507 return performActionForHost(action, arguments); 508 default: 509 return performActionForChild(virtualViewId, action, arguments); 510 } 511 } 512 performActionForHost(int action, Bundle arguments)513 private boolean performActionForHost(int action, Bundle arguments) { 514 return mView.performAccessibilityAction(action, arguments); 515 } 516 performActionForChild(int virtualViewId, int action, Bundle arguments)517 private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) { 518 switch (action) { 519 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: 520 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: 521 return manageFocusForChild(virtualViewId, action); 522 default: 523 return onPerformActionForVirtualView(virtualViewId, action, arguments); 524 } 525 } 526 manageFocusForChild(int virtualViewId, int action)527 private boolean manageFocusForChild(int virtualViewId, int action) { 528 switch (action) { 529 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: 530 return requestAccessibilityFocus(virtualViewId); 531 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: 532 return clearAccessibilityFocus(virtualViewId); 533 default: 534 return false; 535 } 536 } 537 538 /** 539 * Computes whether the specified {@link Rect} intersects with the visible 540 * portion of its parent {@link View}. Modifies {@code localRect} to contain 541 * only the visible portion. 542 * 543 * @param localRect A rectangle in local (parent) coordinates. 544 * @return Whether the specified {@link Rect} is visible on the screen. 545 */ intersectVisibleToUser(Rect localRect)546 private boolean intersectVisibleToUser(Rect localRect) { 547 // Missing or empty bounds mean this view is not visible. 548 if ((localRect == null) || localRect.isEmpty()) { 549 return false; 550 } 551 552 // Attached to invisible window means this view is not visible. 553 if (mView.getWindowVisibility() != View.VISIBLE) { 554 return false; 555 } 556 557 // An invisible predecessor means that this view is not visible. 558 ViewParent viewParent = mView.getParent(); 559 while (viewParent instanceof View) { 560 final View view = (View) viewParent; 561 if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) { 562 return false; 563 } 564 viewParent = view.getParent(); 565 } 566 567 // A null parent implies the view is not visible. 568 if (viewParent == null) { 569 return false; 570 } 571 572 // If no portion of the parent is visible, this view is not visible. 573 if (mTempVisibleRect == null) { 574 mTempVisibleRect = new Rect(); 575 } 576 final Rect tempVisibleRect = mTempVisibleRect; 577 if (!mView.getLocalVisibleRect(tempVisibleRect)) { 578 return false; 579 } 580 581 // Check if the view intersects the visible portion of the parent. 582 return localRect.intersect(tempVisibleRect); 583 } 584 585 /** 586 * Returns whether this virtual view is accessibility focused. 587 * 588 * @return True if the view is accessibility focused. 589 */ isAccessibilityFocused(int virtualViewId)590 private boolean isAccessibilityFocused(int virtualViewId) { 591 return (mFocusedVirtualViewId == virtualViewId); 592 } 593 594 /** 595 * Attempts to give accessibility focus to a virtual view. 596 * <p> 597 * A virtual view will not actually take focus if 598 * {@link AccessibilityManager#isEnabled()} returns false, 599 * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false, 600 * or the view already has accessibility focus. 601 * 602 * @param virtualViewId The id of the virtual view on which to place 603 * accessibility focus. 604 * @return Whether this virtual view actually took accessibility focus. 605 */ requestAccessibilityFocus(int virtualViewId)606 private boolean requestAccessibilityFocus(int virtualViewId) { 607 final AccessibilityManager accessibilityManager = 608 (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 609 610 if (!mManager.isEnabled() 611 || !accessibilityManager.isTouchExplorationEnabled()) { 612 return false; 613 } 614 // TODO: Check virtual view visibility. 615 if (!isAccessibilityFocused(virtualViewId)) { 616 // Clear focus from the previously focused view, if applicable. 617 if (mFocusedVirtualViewId != INVALID_ID) { 618 sendEventForVirtualView(mFocusedVirtualViewId, 619 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 620 } 621 622 // Set focus on the new view. 623 mFocusedVirtualViewId = virtualViewId; 624 625 // TODO: Only invalidate virtual view bounds. 626 mView.invalidate(); 627 sendEventForVirtualView(virtualViewId, 628 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 629 return true; 630 } 631 return false; 632 } 633 634 /** 635 * Attempts to clear accessibility focus from a virtual view. 636 * 637 * @param virtualViewId The id of the virtual view from which to clear 638 * accessibility focus. 639 * @return Whether this virtual view actually cleared accessibility focus. 640 */ clearAccessibilityFocus(int virtualViewId)641 private boolean clearAccessibilityFocus(int virtualViewId) { 642 if (isAccessibilityFocused(virtualViewId)) { 643 mFocusedVirtualViewId = INVALID_ID; 644 mView.invalidate(); 645 sendEventForVirtualView(virtualViewId, 646 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 647 return true; 648 } 649 return false; 650 } 651 652 /** 653 * Provides a mapping between view-relative coordinates and logical 654 * items. 655 * 656 * @param x The view-relative x coordinate 657 * @param y The view-relative y coordinate 658 * @return virtual view identifier for the logical item under 659 * coordinates (x,y) 660 */ getVirtualViewAt(float x, float y)661 protected abstract int getVirtualViewAt(float x, float y); 662 663 /** 664 * Populates a list with the view's visible items. The ordering of items 665 * within {@code virtualViewIds} specifies order of accessibility focus 666 * traversal. 667 * 668 * @param virtualViewIds The list to populate with visible items 669 */ getVisibleVirtualViews(IntArray virtualViewIds)670 protected abstract void getVisibleVirtualViews(IntArray virtualViewIds); 671 672 /** 673 * Populates an {@link AccessibilityEvent} with information about the 674 * specified item. 675 * <p> 676 * Implementations <b>must</b> populate the following required fields: 677 * <ul> 678 * <li>event text, see {@link AccessibilityEvent#getText} or 679 * {@link AccessibilityEvent#setContentDescription} 680 * </ul> 681 * <p> 682 * The helper class automatically populates the following fields with 683 * default values, but implementations may optionally override them: 684 * <ul> 685 * <li>item class name, set to android.view.View, see 686 * {@link AccessibilityEvent#setClassName} 687 * </ul> 688 * <p> 689 * The following required fields are automatically populated by the 690 * helper class and may not be overridden: 691 * <ul> 692 * <li>package name, set to the package of the host view's 693 * {@link Context}, see {@link AccessibilityEvent#setPackageName} 694 * <li>event source, set to the host view and virtual view identifier, 695 * see {@link android.view.accessibility.AccessibilityRecord#setSource(View, int)} 696 * </ul> 697 * 698 * @param virtualViewId The virtual view id for the item for which to 699 * populate the event 700 * @param event The event to populate 701 */ onPopulateEventForVirtualView( int virtualViewId, AccessibilityEvent event)702 protected abstract void onPopulateEventForVirtualView( 703 int virtualViewId, AccessibilityEvent event); 704 705 /** 706 * Populates an {@link AccessibilityEvent} with information about the host 707 * view. 708 * <p> 709 * The default implementation is a no-op. 710 * 711 * @param event the event to populate with information about the host view 712 */ onPopulateEventForHost(AccessibilityEvent event)713 protected void onPopulateEventForHost(AccessibilityEvent event) { 714 // Default implementation is no-op. 715 } 716 717 /** 718 * Populates an {@link AccessibilityNodeInfo} with information 719 * about the specified item. 720 * <p> 721 * Implementations <b>must</b> populate the following required fields: 722 * <ul> 723 * <li>event text, see {@link AccessibilityNodeInfo#setText} or 724 * {@link AccessibilityNodeInfo#setContentDescription} 725 * <li>bounds in parent coordinates, see 726 * {@link AccessibilityNodeInfo#setBoundsInParent} 727 * </ul> 728 * <p> 729 * The helper class automatically populates the following fields with 730 * default values, but implementations may optionally override them: 731 * <ul> 732 * <li>enabled state, set to true, see 733 * {@link AccessibilityNodeInfo#setEnabled} 734 * <li>item class name, identical to the class name set by 735 * {@link #onPopulateEventForVirtualView}, see 736 * {@link AccessibilityNodeInfo#setClassName} 737 * </ul> 738 * <p> 739 * The following required fields are automatically populated by the 740 * helper class and may not be overridden: 741 * <ul> 742 * <li>package name, identical to the package name set by 743 * {@link #onPopulateEventForVirtualView}, see 744 * {@link AccessibilityNodeInfo#setPackageName} 745 * <li>node source, identical to the event source set in 746 * {@link #onPopulateEventForVirtualView}, see 747 * {@link AccessibilityNodeInfo#setSource(View, int)} 748 * <li>parent view, set to the host view, see 749 * {@link AccessibilityNodeInfo#setParent(View)} 750 * <li>visibility, computed based on parent-relative bounds, see 751 * {@link AccessibilityNodeInfo#setVisibleToUser} 752 * <li>accessibility focus, computed based on internal helper state, see 753 * {@link AccessibilityNodeInfo#setAccessibilityFocused} 754 * <li>bounds in screen coordinates, computed based on host view bounds, 755 * see {@link AccessibilityNodeInfo#setBoundsInScreen} 756 * </ul> 757 * <p> 758 * Additionally, the helper class automatically handles accessibility 759 * focus management by adding the appropriate 760 * {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} or 761 * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS} 762 * action. Implementations must <b>never</b> manually add these actions. 763 * <p> 764 * The helper class also automatically modifies parent- and 765 * screen-relative bounds to reflect the portion of the item visible 766 * within its parent. 767 * 768 * @param virtualViewId The virtual view identifier of the item for 769 * which to populate the node 770 * @param node The node to populate 771 */ onPopulateNodeForVirtualView( int virtualViewId, AccessibilityNodeInfo node)772 protected abstract void onPopulateNodeForVirtualView( 773 int virtualViewId, AccessibilityNodeInfo node); 774 775 /** 776 * Populates an {@link AccessibilityNodeInfo} with information about the 777 * host view. 778 * <p> 779 * The default implementation is a no-op. 780 * 781 * @param node the node to populate with information about the host view 782 */ onPopulateNodeForHost(AccessibilityNodeInfo node)783 protected void onPopulateNodeForHost(AccessibilityNodeInfo node) { 784 // Default implementation is no-op. 785 } 786 787 /** 788 * Performs the specified accessibility action on the item associated 789 * with the virtual view identifier. See 790 * {@link AccessibilityNodeInfo#performAction(int, Bundle)} for 791 * more information. 792 * <p> 793 * Implementations <b>must</b> handle any actions added manually in 794 * {@link #onPopulateNodeForVirtualView}. 795 * <p> 796 * The helper class automatically handles focus management resulting 797 * from {@link AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} 798 * and 799 * {@link AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS} 800 * actions. 801 * 802 * @param virtualViewId The virtual view identifier of the item on which 803 * to perform the action 804 * @param action The accessibility action to perform 805 * @param arguments (Optional) A bundle with additional arguments, or 806 * null 807 * @return true if the action was performed 808 */ onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments)809 protected abstract boolean onPerformActionForVirtualView( 810 int virtualViewId, int action, Bundle arguments); 811 812 /** 813 * Exposes a virtual view hierarchy to the accessibility framework. Only 814 * used in API 16+. 815 */ 816 private class ExploreByTouchNodeProvider extends AccessibilityNodeProvider { 817 @Override createAccessibilityNodeInfo(int virtualViewId)818 public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { 819 return ExploreByTouchHelper.this.createNode(virtualViewId); 820 } 821 822 @Override performAction(int virtualViewId, int action, Bundle arguments)823 public boolean performAction(int virtualViewId, int action, Bundle arguments) { 824 return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments); 825 } 826 } 827 } 828