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 android.support.v4.widget; 18 19import android.content.Context; 20import android.graphics.Rect; 21import android.os.Bundle; 22import android.support.annotation.NonNull; 23import android.support.annotation.Nullable; 24import android.support.v4.util.SparseArrayCompat; 25import android.support.v4.view.AccessibilityDelegateCompat; 26import android.support.v4.view.ViewCompat; 27import android.support.v4.view.ViewCompat.FocusDirection; 28import android.support.v4.view.ViewCompat.FocusRealDirection; 29import android.support.v4.view.ViewParentCompat; 30import android.support.v4.view.accessibility.AccessibilityEventCompat; 31import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 32import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; 33import android.support.v4.view.accessibility.AccessibilityRecordCompat; 34import android.view.KeyEvent; 35import android.view.MotionEvent; 36import android.view.View; 37import android.view.ViewParent; 38import android.view.accessibility.AccessibilityEvent; 39import android.view.accessibility.AccessibilityManager; 40import android.view.accessibility.AccessibilityRecord; 41 42import java.util.ArrayList; 43import java.util.List; 44 45/** 46 * ExploreByTouchHelper is a utility class for implementing accessibility 47 * support in custom {@link View}s that represent a collection of View-like 48 * logical items. It extends {@link AccessibilityNodeProviderCompat} and 49 * simplifies many aspects of providing information to accessibility services 50 * and managing accessibility focus. 51 * <p> 52 * Clients should override abstract methods on this class and attach it to the 53 * host view using {@link ViewCompat#setAccessibilityDelegate}: 54 * <p> 55 * <pre> 56 * class MyCustomView extends View { 57 * private MyVirtualViewHelper mVirtualViewHelper; 58 * 59 * public MyCustomView(Context context, ...) { 60 * ... 61 * mVirtualViewHelper = new MyVirtualViewHelper(this); 62 * ViewCompat.setAccessibilityDelegate(this, mVirtualViewHelper); 63 * } 64 * 65 * @Override 66 * public boolean dispatchHoverEvent(MotionEvent event) { 67 * return mHelper.dispatchHoverEvent(this, event) 68 * || super.dispatchHoverEvent(event); 69 * } 70 * 71 * @Override 72 * public boolean dispatchKeyEvent(KeyEvent event) { 73 * return mHelper.dispatchKeyEvent(event) 74 * || super.dispatchKeyEvent(event); 75 * } 76 * 77 * @Override 78 * public boolean onFocusChanged(boolean gainFocus, int direction, 79 * Rect previouslyFocusedRect) { 80 * super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 81 * mHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 82 * } 83 * } 84 * mAccessHelper = new MyExploreByTouchHelper(someView); 85 * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper); 86 * </pre> 87 */ 88public abstract class ExploreByTouchHelper extends AccessibilityDelegateCompat { 89 /** Virtual node identifier value for invalid nodes. */ 90 public static final int INVALID_ID = Integer.MIN_VALUE; 91 92 /** Virtual node identifier value for the host view's node. */ 93 public static final int HOST_ID = View.NO_ID; 94 95 /** Default class name used for virtual views. */ 96 private static final String DEFAULT_CLASS_NAME = "android.view.View"; 97 98 /** Default bounds used to determine if the client didn't set any. */ 99 private static final Rect INVALID_PARENT_BOUNDS = new Rect( 100 Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); 101 102 // Temporary, reusable data structures. 103 private final Rect mTempScreenRect = new Rect(); 104 private final Rect mTempParentRect = new Rect(); 105 private final Rect mTempVisibleRect = new Rect(); 106 private final int[] mTempGlobalRect = new int[2]; 107 108 /** System accessibility manager, used to check state and send events. */ 109 private final AccessibilityManager mManager; 110 111 /** View whose internal structure is exposed through this helper. */ 112 private final View mHost; 113 114 /** Virtual node provider used to expose logical structure to services. */ 115 private MyNodeProvider mNodeProvider; 116 117 /** Identifier for the virtual view that holds accessibility focus. */ 118 private int mAccessibilityFocusedVirtualViewId = INVALID_ID; 119 120 /** Identifier for the virtual view that holds keyboard focus. */ 121 private int mKeyboardFocusedVirtualViewId = INVALID_ID; 122 123 /** Identifier for the virtual view that is currently hovered. */ 124 private int mHoveredVirtualViewId = INVALID_ID; 125 126 /** 127 * Constructs a new helper that can expose a virtual view hierarchy for the 128 * specified host view. 129 * 130 * @param host view whose virtual view hierarchy is exposed by this helper 131 */ 132 public ExploreByTouchHelper(View host) { 133 if (host == null) { 134 throw new IllegalArgumentException("View may not be null"); 135 } 136 137 mHost = host; 138 139 final Context context = host.getContext(); 140 mManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 141 142 // Host view must be focusable so that we can delegate to virtual 143 // views. 144 host.setFocusable(true); 145 if (ViewCompat.getImportantForAccessibility(host) 146 == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 147 ViewCompat.setImportantForAccessibility( 148 host, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 149 } 150 } 151 152 @Override 153 public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { 154 if (mNodeProvider == null) { 155 mNodeProvider = new MyNodeProvider(); 156 } 157 return mNodeProvider; 158 } 159 160 /** 161 * Delegates hover events from the host view. 162 * <p> 163 * Dispatches hover {@link MotionEvent}s to the virtual view hierarchy when 164 * the Explore by Touch feature is enabled. 165 * <p> 166 * This method should be called by overriding the host view's 167 * {@link View#dispatchHoverEvent(MotionEvent)} method: 168 * <pre>@Override 169 * public boolean dispatchHoverEvent(MotionEvent event) { 170 * return mHelper.dispatchHoverEvent(this, event) 171 * || super.dispatchHoverEvent(event); 172 * } 173 * </pre> 174 * 175 * @param event The hover event to dispatch to the virtual view hierarchy. 176 * @return Whether the hover event was handled. 177 */ 178 public final boolean dispatchHoverEvent(@NonNull MotionEvent event) { 179 if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) { 180 return false; 181 } 182 183 switch (event.getAction()) { 184 case MotionEvent.ACTION_HOVER_MOVE: 185 case MotionEvent.ACTION_HOVER_ENTER: 186 final int virtualViewId = getVirtualViewAt(event.getX(), event.getY()); 187 updateHoveredVirtualView(virtualViewId); 188 return (virtualViewId != INVALID_ID); 189 case MotionEvent.ACTION_HOVER_EXIT: 190 if (mAccessibilityFocusedVirtualViewId != INVALID_ID) { 191 updateHoveredVirtualView(INVALID_ID); 192 return true; 193 } 194 return false; 195 default: 196 return false; 197 } 198 } 199 200 /** 201 * Delegates key events from the host view. 202 * <p> 203 * This method should be called by overriding the host view's 204 * {@link View#dispatchKeyEvent(KeyEvent)} method: 205 * <pre>@Override 206 * public boolean dispatchKeyEvent(KeyEvent event) { 207 * return mHelper.dispatchKeyEvent(event) 208 * || super.dispatchKeyEvent(event); 209 * } 210 * </pre> 211 */ 212 public final boolean dispatchKeyEvent(@NonNull KeyEvent event) { 213 boolean handled = false; 214 215 final int action = event.getAction(); 216 if (action != KeyEvent.ACTION_UP) { 217 final int keyCode = event.getKeyCode(); 218 switch (keyCode) { 219 case KeyEvent.KEYCODE_DPAD_LEFT: 220 case KeyEvent.KEYCODE_DPAD_UP: 221 case KeyEvent.KEYCODE_DPAD_RIGHT: 222 case KeyEvent.KEYCODE_DPAD_DOWN: 223 if (event.hasNoModifiers()) { 224 final int direction = keyToDirection(keyCode); 225 final int count = 1 + event.getRepeatCount(); 226 for (int i = 0; i < count; i++) { 227 if (moveFocus(direction, null)) { 228 handled = true; 229 } else { 230 break; 231 } 232 } 233 } 234 break; 235 case KeyEvent.KEYCODE_DPAD_CENTER: 236 case KeyEvent.KEYCODE_ENTER: 237 if (event.hasNoModifiers()) { 238 if (event.getRepeatCount() == 0) { 239 clickKeyboardFocusedVirtualView(); 240 handled = true; 241 } 242 } 243 break; 244 case KeyEvent.KEYCODE_TAB: 245 if (event.hasNoModifiers()) { 246 handled = moveFocus(View.FOCUS_FORWARD, null); 247 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 248 handled = moveFocus(View.FOCUS_BACKWARD, null); 249 } 250 break; 251 } 252 } 253 254 return handled; 255 } 256 257 /** 258 * Delegates focus changes from the host view. 259 * <p> 260 * This method should be called by overriding the host view's 261 * {@link View#onFocusChanged(boolean, int, Rect)} method: 262 * <pre>@Override 263 * public boolean onFocusChanged(boolean gainFocus, int direction, 264 * Rect previouslyFocusedRect) { 265 * super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 266 * mHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 267 * } 268 * </pre> 269 */ 270 public final void onFocusChanged(boolean gainFocus, int direction, 271 @Nullable Rect previouslyFocusedRect) { 272 if (mKeyboardFocusedVirtualViewId != INVALID_ID) { 273 clearKeyboardFocusForVirtualView(mKeyboardFocusedVirtualViewId); 274 } 275 276 if (gainFocus) { 277 moveFocus(direction, previouslyFocusedRect); 278 } 279 } 280 281 /** 282 * @return the identifier of the virtual view that has accessibility focus 283 * or {@link #INVALID_ID} if no virtual view has accessibility 284 * focus 285 */ 286 public final int getAccessibilityFocusedVirtualViewId() { 287 return mAccessibilityFocusedVirtualViewId; 288 } 289 290 /** 291 * @return the identifier of the virtual view that has keyboard focus 292 * or {@link #INVALID_ID} if no virtual view has keyboard focus 293 */ 294 public final int getKeyboardFocusedVirtualViewId() { 295 return mKeyboardFocusedVirtualViewId; 296 } 297 298 /** 299 * Maps key event codes to focus directions. 300 * 301 * @param keyCode the key event code 302 * @return the corresponding focus direction 303 */ 304 @FocusRealDirection 305 private static int keyToDirection(int keyCode) { 306 switch (keyCode) { 307 case KeyEvent.KEYCODE_DPAD_LEFT: 308 return View.FOCUS_LEFT; 309 case KeyEvent.KEYCODE_DPAD_UP: 310 return View.FOCUS_UP; 311 case KeyEvent.KEYCODE_DPAD_RIGHT: 312 return View.FOCUS_RIGHT; 313 default: 314 return View.FOCUS_DOWN; 315 } 316 } 317 318 /** 319 * Obtains the bounds for the specified virtual view. 320 * 321 * @param virtualViewId the identifier of the virtual view 322 * @param outBounds the rect to populate with virtual view bounds 323 */ 324 private void getBoundsInParent(int virtualViewId, Rect outBounds) { 325 final AccessibilityNodeInfoCompat node = obtainAccessibilityNodeInfo(virtualViewId); 326 node.getBoundsInParent(outBounds); 327 } 328 329 /** 330 * Adapts AccessibilityNodeInfoCompat for obtaining bounds. 331 */ 332 private static final FocusStrategy.BoundsAdapter<AccessibilityNodeInfoCompat> NODE_ADAPTER = 333 new FocusStrategy.BoundsAdapter<AccessibilityNodeInfoCompat>() { 334 @Override 335 public void obtainBounds(AccessibilityNodeInfoCompat node, Rect outBounds) { 336 node.getBoundsInParent(outBounds); 337 } 338 }; 339 340 /** 341 * Adapts SparseArrayCompat for iterating through values. 342 */ 343 private static final FocusStrategy.CollectionAdapter<SparseArrayCompat< 344 AccessibilityNodeInfoCompat>, AccessibilityNodeInfoCompat> SPARSE_VALUES_ADAPTER = 345 new FocusStrategy.CollectionAdapter<SparseArrayCompat< 346 AccessibilityNodeInfoCompat>, AccessibilityNodeInfoCompat>() { 347 @Override 348 public AccessibilityNodeInfoCompat get( 349 SparseArrayCompat<AccessibilityNodeInfoCompat> collection, int index) { 350 return collection.valueAt(index); 351 } 352 353 @Override 354 public int size(SparseArrayCompat<AccessibilityNodeInfoCompat> collection) { 355 return collection.size(); 356 } 357 }; 358 359 /** 360 * Attempts to move keyboard focus in the specified direction. 361 * 362 * @param direction the direction in which to move keyboard focus 363 * @param previouslyFocusedRect the bounds of the previously focused item, 364 * or {@code null} if not available 365 * @return {@code true} if keyboard focus moved to a virtual view managed 366 * by this helper, or {@code false} otherwise 367 */ 368 private boolean moveFocus(@FocusDirection int direction, @Nullable Rect previouslyFocusedRect) { 369 final SparseArrayCompat<AccessibilityNodeInfoCompat> allNodes = getAllNodes(); 370 371 final int focusedNodeId = mKeyboardFocusedVirtualViewId; 372 final AccessibilityNodeInfoCompat focusedNode = 373 focusedNodeId == INVALID_ID ? null : allNodes.get(focusedNodeId); 374 375 final AccessibilityNodeInfoCompat nextFocusedNode; 376 switch (direction) { 377 case View.FOCUS_FORWARD: 378 case View.FOCUS_BACKWARD: 379 final boolean isLayoutRtl = 380 ViewCompat.getLayoutDirection(mHost) == ViewCompat.LAYOUT_DIRECTION_RTL; 381 nextFocusedNode = FocusStrategy.findNextFocusInRelativeDirection(allNodes, 382 SPARSE_VALUES_ADAPTER, NODE_ADAPTER, focusedNode, direction, isLayoutRtl, 383 false); 384 break; 385 case View.FOCUS_LEFT: 386 case View.FOCUS_UP: 387 case View.FOCUS_RIGHT: 388 case View.FOCUS_DOWN: 389 final Rect selectedRect = new Rect(); 390 if (mKeyboardFocusedVirtualViewId != INVALID_ID) { 391 // Focus is moving from a virtual view within the host. 392 getBoundsInParent(mKeyboardFocusedVirtualViewId, selectedRect); 393 } else if (previouslyFocusedRect != null) { 394 // Focus is moving from a real view outside the host. 395 selectedRect.set(previouslyFocusedRect); 396 } else { 397 // Focus is moving from... somewhere? Make a guess. 398 // Usually this happens when another view was too lazy 399 // to pass the previously focused rect (ex. ScrollView 400 // when moving UP or DOWN). 401 guessPreviouslyFocusedRect(mHost, direction, selectedRect); 402 } 403 nextFocusedNode = FocusStrategy.findNextFocusInAbsoluteDirection(allNodes, 404 SPARSE_VALUES_ADAPTER, NODE_ADAPTER, focusedNode, selectedRect, direction); 405 break; 406 default: 407 throw new IllegalArgumentException("direction must be one of " 408 + "{FOCUS_FORWARD, FOCUS_BACKWARD, FOCUS_UP, FOCUS_DOWN, " 409 + "FOCUS_LEFT, FOCUS_RIGHT}."); 410 } 411 412 final int nextFocusedNodeId; 413 if (nextFocusedNode == null) { 414 nextFocusedNodeId = INVALID_ID; 415 } else { 416 final int index = allNodes.indexOfValue(nextFocusedNode); 417 nextFocusedNodeId = allNodes.keyAt(index); 418 } 419 420 return requestKeyboardFocusForVirtualView(nextFocusedNodeId); 421 } 422 423 private SparseArrayCompat<AccessibilityNodeInfoCompat> getAllNodes() { 424 final List<Integer> virtualViewIds = new ArrayList<>(); 425 getVisibleVirtualViews(virtualViewIds); 426 427 final SparseArrayCompat<AccessibilityNodeInfoCompat> allNodes = new SparseArrayCompat<>(); 428 for (int virtualViewId = 0; virtualViewId < virtualViewIds.size(); virtualViewId++) { 429 final AccessibilityNodeInfoCompat virtualView = createNodeForChild(virtualViewId); 430 allNodes.put(virtualViewId, virtualView); 431 } 432 433 return allNodes; 434 } 435 436 /** 437 * Obtains a best guess for the previously focused rect for keyboard focus 438 * moving in the specified direction. 439 * 440 * @param host the view into which focus is moving 441 * @param direction the absolute direction in which focus is moving 442 * @param outBounds the rect to populate with the best-guess bounds for the 443 * previous focus rect 444 */ 445 private static Rect guessPreviouslyFocusedRect(@NonNull View host, 446 @FocusRealDirection int direction, @NonNull Rect outBounds) { 447 final int w = host.getWidth(); 448 final int h = host.getHeight(); 449 450 switch (direction) { 451 case View.FOCUS_LEFT: 452 outBounds.set(w, 0, w, h); 453 break; 454 case View.FOCUS_UP: 455 outBounds.set(0, h, w, h); 456 break; 457 case View.FOCUS_RIGHT: 458 outBounds.set(-1, 0, -1, h); 459 break; 460 case View.FOCUS_DOWN: 461 outBounds.set(0, -1, w, -1); 462 break; 463 default: 464 throw new IllegalArgumentException("direction must be one of " 465 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 466 } 467 468 return outBounds; 469 } 470 471 /** 472 * Performs a click action on the keyboard focused virtual view, if any. 473 * 474 * @return {@code true} if the click action was performed successfully or 475 * {@code false} otherwise 476 */ 477 private boolean clickKeyboardFocusedVirtualView() { 478 return mKeyboardFocusedVirtualViewId != INVALID_ID && onPerformActionForVirtualView( 479 mKeyboardFocusedVirtualViewId, AccessibilityNodeInfoCompat.ACTION_CLICK, null); 480 } 481 482 /** 483 * Populates an event of the specified type with information about an item 484 * and attempts to send it up through the view hierarchy. 485 * <p> 486 * You should call this method after performing a user action that normally 487 * fires an accessibility event, such as clicking on an item. 488 * <p> 489 * <pre>public void performItemClick(T item) { 490 * ... 491 * sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED); 492 * } 493 * </pre> 494 * 495 * @param virtualViewId the identifier of the virtual view for which to 496 * send an event 497 * @param eventType the type of event to send 498 * @return {@code true} if the event was sent successfully, {@code false} 499 * otherwise 500 */ 501 public final boolean sendEventForVirtualView(int virtualViewId, int eventType) { 502 if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) { 503 return false; 504 } 505 506 final ViewParent parent = mHost.getParent(); 507 if (parent == null) { 508 return false; 509 } 510 511 final AccessibilityEvent event = createEvent(virtualViewId, eventType); 512 return ViewParentCompat.requestSendAccessibilityEvent(parent, mHost, event); 513 } 514 515 /** 516 * Notifies the accessibility framework that the properties of the parent 517 * view have changed. 518 * <p> 519 * You <strong>must</strong> call this method after adding or removing 520 * items from the parent view. 521 */ 522 public final void invalidateRoot() { 523 invalidateVirtualView(HOST_ID, AccessibilityEventCompat.CONTENT_CHANGE_TYPE_SUBTREE); 524 } 525 526 /** 527 * Notifies the accessibility framework that the properties of a particular 528 * item have changed. 529 * <p> 530 * You <strong>must</strong> call this method after changing any of the 531 * properties set in 532 * {@link #onPopulateNodeForVirtualView(int, AccessibilityNodeInfoCompat)}. 533 * 534 * @param virtualViewId the virtual view id to invalidate, or 535 * {@link #HOST_ID} to invalidate the root view 536 * @see #invalidateVirtualView(int, int) 537 */ 538 public final void invalidateVirtualView(int virtualViewId) { 539 invalidateVirtualView(virtualViewId, 540 AccessibilityEventCompat.CONTENT_CHANGE_TYPE_UNDEFINED); 541 } 542 543 /** 544 * Notifies the accessibility framework that the properties of a particular 545 * item have changed. 546 * <p> 547 * You <strong>must</strong> call this method after changing any of the 548 * properties set in 549 * {@link #onPopulateNodeForVirtualView(int, AccessibilityNodeInfoCompat)}. 550 * 551 * @param virtualViewId the virtual view id to invalidate, or 552 * {@link #HOST_ID} to invalidate the root view 553 * @param changeTypes the bit mask of change types. May be {@code 0} for the 554 * default (undefined) change type or one or more of: 555 * <ul> 556 * <li>{@link AccessibilityEventCompat#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} 557 * <li>{@link AccessibilityEventCompat#CONTENT_CHANGE_TYPE_SUBTREE} 558 * <li>{@link AccessibilityEventCompat#CONTENT_CHANGE_TYPE_TEXT} 559 * <li>{@link AccessibilityEventCompat#CONTENT_CHANGE_TYPE_UNDEFINED} 560 * </ul> 561 */ 562 public final void invalidateVirtualView(int virtualViewId, int changeTypes) { 563 if (virtualViewId != INVALID_ID && mManager.isEnabled()) { 564 final ViewParent parent = mHost.getParent(); 565 if (parent != null) { 566 // Send events up the hierarchy so they can be coalesced. 567 final AccessibilityEvent event = createEvent(virtualViewId, 568 AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); 569 AccessibilityEventCompat.setContentChangeTypes(event, changeTypes); 570 ViewParentCompat.requestSendAccessibilityEvent(parent, mHost, event); 571 } 572 } 573 } 574 575 /** 576 * Returns the virtual view ID for the currently accessibility focused 577 * item. 578 * 579 * @return the identifier of the virtual view that has accessibility focus 580 * or {@link #INVALID_ID} if no virtual view has accessibility 581 * focus 582 * @deprecated Use {@link #getAccessibilityFocusedVirtualViewId()}. 583 */ 584 @Deprecated 585 public int getFocusedVirtualView() { 586 return getAccessibilityFocusedVirtualViewId(); 587 } 588 589 /** 590 * Called when the focus state of a virtual view changes. 591 * 592 * @param virtualViewId the virtual view identifier 593 * @param hasFocus {@code true} if the view has focus, {@code false} 594 * otherwise 595 */ 596 protected void onVirtualViewKeyboardFocusChanged(int virtualViewId, boolean hasFocus) { 597 // Stub method. 598 } 599 600 /** 601 * Sets the currently hovered item, sending hover accessibility events as 602 * necessary to maintain the correct state. 603 * 604 * @param virtualViewId the virtual view id for the item currently being 605 * hovered, or {@link #INVALID_ID} if no item is 606 * hovered within the parent view 607 */ 608 private void updateHoveredVirtualView(int virtualViewId) { 609 if (mHoveredVirtualViewId == virtualViewId) { 610 return; 611 } 612 613 final int previousVirtualViewId = mHoveredVirtualViewId; 614 mHoveredVirtualViewId = virtualViewId; 615 616 // Stay consistent with framework behavior by sending ENTER/EXIT pairs 617 // in reverse order. This is accurate as of API 18. 618 sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 619 sendEventForVirtualView( 620 previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 621 } 622 623 /** 624 * Constructs and returns an {@link AccessibilityEvent} for the specified 625 * virtual view id, which includes the host view ({@link #HOST_ID}). 626 * 627 * @param virtualViewId the virtual view id for the item for which to 628 * construct an event 629 * @param eventType the type of event to construct 630 * @return an {@link AccessibilityEvent} populated with information about 631 * the specified item 632 */ 633 private AccessibilityEvent createEvent(int virtualViewId, int eventType) { 634 switch (virtualViewId) { 635 case HOST_ID: 636 return createEventForHost(eventType); 637 default: 638 return createEventForChild(virtualViewId, eventType); 639 } 640 } 641 642 /** 643 * Constructs and returns an {@link AccessibilityEvent} for the host node. 644 * 645 * @param eventType the type of event to construct 646 * @return an {@link AccessibilityEvent} populated with information about 647 * the specified item 648 */ 649 private AccessibilityEvent createEventForHost(int eventType) { 650 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 651 mHost.onInitializeAccessibilityEvent(event); 652 return event; 653 } 654 655 @Override 656 public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 657 super.onInitializeAccessibilityEvent(host, event); 658 659 // Allow the client to populate the event. 660 onPopulateEventForHost(event); 661 } 662 663 /** 664 * Constructs and returns an {@link AccessibilityEvent} populated with 665 * information about the specified item. 666 * 667 * @param virtualViewId the virtual view id for the item for which to 668 * construct an event 669 * @param eventType the type of event to construct 670 * @return an {@link AccessibilityEvent} populated with information about 671 * the specified item 672 */ 673 private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) { 674 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 675 final AccessibilityNodeInfoCompat node = obtainAccessibilityNodeInfo(virtualViewId); 676 677 // Allow the client to override these properties, 678 event.getText().add(node.getText()); 679 event.setContentDescription(node.getContentDescription()); 680 event.setScrollable(node.isScrollable()); 681 event.setPassword(node.isPassword()); 682 event.setEnabled(node.isEnabled()); 683 event.setChecked(node.isChecked()); 684 685 // Allow the client to populate the event. 686 onPopulateEventForVirtualView(virtualViewId, event); 687 688 // Make sure the developer is following the rules. 689 if (event.getText().isEmpty() && (event.getContentDescription() == null)) { 690 throw new RuntimeException("Callbacks must add text or a content description in " 691 + "populateEventForVirtualViewId()"); 692 } 693 694 // Don't allow the client to override these properties. 695 event.setClassName(node.getClassName()); 696 AccessibilityRecordCompat.setSource(event, mHost, virtualViewId); 697 event.setPackageName(mHost.getContext().getPackageName()); 698 699 return event; 700 } 701 702 /** 703 * Obtains a populated {@link AccessibilityNodeInfoCompat} for the 704 * virtual view with the specified identifier. 705 * <p> 706 * This method may be called with identifier {@link #HOST_ID} to obtain a 707 * node for the host view. 708 * 709 * @param virtualViewId the identifier of the virtual view for which to 710 * construct a node 711 * @return an {@link AccessibilityNodeInfoCompat} populated with information 712 * about the specified item 713 */ 714 @NonNull 715 AccessibilityNodeInfoCompat obtainAccessibilityNodeInfo(int virtualViewId) { 716 if (virtualViewId == HOST_ID) { 717 return createNodeForHost(); 718 } 719 720 return createNodeForChild(virtualViewId); 721 } 722 723 /** 724 * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the 725 * host view populated with its virtual descendants. 726 * 727 * @return an {@link AccessibilityNodeInfoCompat} for the parent node 728 */ 729 @NonNull 730 private AccessibilityNodeInfoCompat createNodeForHost() { 731 final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(mHost); 732 ViewCompat.onInitializeAccessibilityNodeInfo(mHost, info); 733 734 // Add the virtual descendants. 735 final ArrayList<Integer> virtualViewIds = new ArrayList<>(); 736 getVisibleVirtualViews(virtualViewIds); 737 738 final int realNodeCount = info.getChildCount(); 739 if (realNodeCount > 0 && virtualViewIds.size() > 0) { 740 throw new RuntimeException("Views cannot have both real and virtual children"); 741 } 742 743 for (int i = 0, count = virtualViewIds.size(); i < count; i++) { 744 info.addChild(mHost, virtualViewIds.get(i)); 745 } 746 747 return info; 748 } 749 750 @Override 751 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 752 super.onInitializeAccessibilityNodeInfo(host, info); 753 754 // Allow the client to populate the host node. 755 onPopulateNodeForHost(info); 756 } 757 758 /** 759 * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the 760 * specified item. Automatically manages accessibility focus actions. 761 * <p> 762 * Allows the implementing class to specify most node properties, but 763 * overrides the following: 764 * <ul> 765 * <li>{@link AccessibilityNodeInfoCompat#setPackageName} 766 * <li>{@link AccessibilityNodeInfoCompat#setClassName} 767 * <li>{@link AccessibilityNodeInfoCompat#setParent(View)} 768 * <li>{@link AccessibilityNodeInfoCompat#setSource(View, int)} 769 * <li>{@link AccessibilityNodeInfoCompat#setVisibleToUser} 770 * <li>{@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)} 771 * </ul> 772 * <p> 773 * Uses the bounds of the parent view and the parent-relative bounding 774 * rectangle specified by 775 * {@link AccessibilityNodeInfoCompat#getBoundsInParent} to automatically 776 * update the following properties: 777 * <ul> 778 * <li>{@link AccessibilityNodeInfoCompat#setVisibleToUser} 779 * <li>{@link AccessibilityNodeInfoCompat#setBoundsInParent} 780 * </ul> 781 * 782 * @param virtualViewId the virtual view id for item for which to construct 783 * a node 784 * @return an {@link AccessibilityNodeInfoCompat} for the specified item 785 */ 786 @NonNull 787 private AccessibilityNodeInfoCompat createNodeForChild(int virtualViewId) { 788 final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain(); 789 790 // Ensure the client has good defaults. 791 node.setEnabled(true); 792 node.setFocusable(true); 793 node.setClassName(DEFAULT_CLASS_NAME); 794 node.setBoundsInParent(INVALID_PARENT_BOUNDS); 795 node.setBoundsInScreen(INVALID_PARENT_BOUNDS); 796 node.setParent(mHost); 797 798 // Allow the client to populate the node. 799 onPopulateNodeForVirtualView(virtualViewId, node); 800 801 // Make sure the developer is following the rules. 802 if ((node.getText() == null) && (node.getContentDescription() == null)) { 803 throw new RuntimeException("Callbacks must add text or a content description in " 804 + "populateNodeForVirtualViewId()"); 805 } 806 807 node.getBoundsInParent(mTempParentRect); 808 if (mTempParentRect.equals(INVALID_PARENT_BOUNDS)) { 809 throw new RuntimeException("Callbacks must set parent bounds in " 810 + "populateNodeForVirtualViewId()"); 811 } 812 813 final int actions = node.getActions(); 814 if ((actions & AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS) != 0) { 815 throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in " 816 + "populateNodeForVirtualViewId()"); 817 } 818 if ((actions & AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) { 819 throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in " 820 + "populateNodeForVirtualViewId()"); 821 } 822 823 // Don't allow the client to override these properties. 824 node.setPackageName(mHost.getContext().getPackageName()); 825 node.setSource(mHost, virtualViewId); 826 827 // Manage internal accessibility focus state. 828 if (mAccessibilityFocusedVirtualViewId == virtualViewId) { 829 node.setAccessibilityFocused(true); 830 node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 831 } else { 832 node.setAccessibilityFocused(false); 833 node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); 834 } 835 836 // Manage internal keyboard focus state. 837 final boolean isFocused = mKeyboardFocusedVirtualViewId == virtualViewId; 838 if (isFocused) { 839 node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS); 840 } else if (node.isFocusable()) { 841 node.addAction(AccessibilityNodeInfoCompat.ACTION_FOCUS); 842 } 843 node.setFocused(isFocused); 844 845 mHost.getLocationOnScreen(mTempGlobalRect); 846 847 // If not explicitly specified, calculate screen-relative bounds and 848 // offset for scroll position based on bounds in parent. 849 node.getBoundsInScreen(mTempScreenRect); 850 if (mTempScreenRect.equals(INVALID_PARENT_BOUNDS)) { 851 node.getBoundsInParent(mTempScreenRect); 852 853 // If there is a parent node, adjust bounds based on the parent node. 854 if (node.mParentVirtualDescendantId != HOST_ID) { 855 AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain(); 856 // Walk up the node tree to adjust the screen rect. 857 for (int virtualDescendantId = node.mParentVirtualDescendantId; 858 virtualDescendantId != HOST_ID; 859 virtualDescendantId = parentNode.mParentVirtualDescendantId) { 860 // Reset the values in the parent node we'll be using. 861 parentNode.setParent(mHost, HOST_ID); 862 parentNode.setBoundsInParent(INVALID_PARENT_BOUNDS); 863 // Adjust the bounds for the parent node. 864 onPopulateNodeForVirtualView(virtualDescendantId, parentNode); 865 parentNode.getBoundsInParent(mTempParentRect); 866 mTempScreenRect.offset(mTempParentRect.left, mTempParentRect.top); 867 } 868 parentNode.recycle(); 869 } 870 // Adjust the rect for the host view's location. 871 mTempScreenRect.offset(mTempGlobalRect[0] - mHost.getScrollX(), 872 mTempGlobalRect[1] - mHost.getScrollY()); 873 } 874 875 if (mHost.getLocalVisibleRect(mTempVisibleRect)) { 876 mTempVisibleRect.offset(mTempGlobalRect[0] - mHost.getScrollX(), 877 mTempGlobalRect[1] - mHost.getScrollY()); 878 final boolean intersects = mTempScreenRect.intersect(mTempVisibleRect); 879 if (intersects) { 880 node.setBoundsInScreen(mTempScreenRect); 881 882 if (isVisibleToUser(mTempScreenRect)) { 883 node.setVisibleToUser(true); 884 } 885 } 886 } 887 888 return node; 889 } 890 891 boolean performAction(int virtualViewId, int action, Bundle arguments) { 892 switch (virtualViewId) { 893 case HOST_ID: 894 return performActionForHost(action, arguments); 895 default: 896 return performActionForChild(virtualViewId, action, arguments); 897 } 898 } 899 900 private boolean performActionForHost(int action, Bundle arguments) { 901 return ViewCompat.performAccessibilityAction(mHost, action, arguments); 902 } 903 904 private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) { 905 switch (action) { 906 case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: 907 return requestAccessibilityFocus(virtualViewId); 908 case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: 909 return clearAccessibilityFocus(virtualViewId); 910 case AccessibilityNodeInfoCompat.ACTION_FOCUS: 911 return requestKeyboardFocusForVirtualView(virtualViewId); 912 case AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS: 913 return clearKeyboardFocusForVirtualView(virtualViewId); 914 default: 915 return onPerformActionForVirtualView(virtualViewId, action, arguments); 916 } 917 } 918 919 /** 920 * Computes whether the specified {@link Rect} intersects with the visible 921 * portion of its parent {@link View}. Modifies {@code localRect} to contain 922 * only the visible portion. 923 * 924 * @param localRect a rectangle in local (parent) coordinates 925 * @return whether the specified {@link Rect} is visible on the screen 926 */ 927 private boolean isVisibleToUser(Rect localRect) { 928 // Missing or empty bounds mean this view is not visible. 929 if ((localRect == null) || localRect.isEmpty()) { 930 return false; 931 } 932 933 // Attached to invisible window means this view is not visible. 934 if (mHost.getWindowVisibility() != View.VISIBLE) { 935 return false; 936 } 937 938 // An invisible predecessor means that this view is not visible. 939 ViewParent viewParent = mHost.getParent(); 940 while (viewParent instanceof View) { 941 final View view = (View) viewParent; 942 if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) { 943 return false; 944 } 945 viewParent = view.getParent(); 946 } 947 948 // A null parent implies the view is not visible. 949 return viewParent != null; 950 } 951 952 /** 953 * Attempts to give accessibility focus to a virtual view. 954 * <p> 955 * A virtual view will not actually take focus if 956 * {@link AccessibilityManager#isEnabled()} returns false, 957 * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false, 958 * or the view already has accessibility focus. 959 * 960 * @param virtualViewId the identifier of the virtual view on which to 961 * place accessibility focus 962 * @return whether this virtual view actually took accessibility focus 963 */ 964 private boolean requestAccessibilityFocus(int virtualViewId) { 965 if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) { 966 return false; 967 } 968 // TODO: Check virtual view visibility. 969 if (mAccessibilityFocusedVirtualViewId != virtualViewId) { 970 // Clear focus from the previously focused view, if applicable. 971 if (mAccessibilityFocusedVirtualViewId != INVALID_ID) { 972 clearAccessibilityFocus(mAccessibilityFocusedVirtualViewId); 973 } 974 975 // Set focus on the new view. 976 mAccessibilityFocusedVirtualViewId = virtualViewId; 977 978 // TODO: Only invalidate virtual view bounds. 979 mHost.invalidate(); 980 sendEventForVirtualView(virtualViewId, 981 AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 982 return true; 983 } 984 return false; 985 } 986 987 /** 988 * Attempts to clear accessibility focus from a virtual view. 989 * 990 * @param virtualViewId the identifier of the virtual view from which to 991 * clear accessibility focus 992 * @return whether this virtual view actually cleared accessibility focus 993 */ 994 private boolean clearAccessibilityFocus(int virtualViewId) { 995 if (mAccessibilityFocusedVirtualViewId == virtualViewId) { 996 mAccessibilityFocusedVirtualViewId = INVALID_ID; 997 mHost.invalidate(); 998 sendEventForVirtualView(virtualViewId, 999 AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 1000 return true; 1001 } 1002 return false; 1003 } 1004 1005 /** 1006 * Attempts to give keyboard focus to a virtual view. 1007 * 1008 * @param virtualViewId the identifier of the virtual view on which to 1009 * place keyboard focus 1010 * @return whether this virtual view actually took keyboard focus 1011 */ 1012 public final boolean requestKeyboardFocusForVirtualView(int virtualViewId) { 1013 if (!mHost.isFocused() && !mHost.requestFocus()) { 1014 // Host must have real keyboard focus. 1015 return false; 1016 } 1017 1018 if (mKeyboardFocusedVirtualViewId == virtualViewId) { 1019 // The virtual view already has focus. 1020 return false; 1021 } 1022 1023 if (mKeyboardFocusedVirtualViewId != INVALID_ID) { 1024 clearKeyboardFocusForVirtualView(mKeyboardFocusedVirtualViewId); 1025 } 1026 1027 mKeyboardFocusedVirtualViewId = virtualViewId; 1028 1029 onVirtualViewKeyboardFocusChanged(virtualViewId, true); 1030 sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_FOCUSED); 1031 1032 return true; 1033 } 1034 1035 /** 1036 * Attempts to clear keyboard focus from a virtual view. 1037 * 1038 * @param virtualViewId the identifier of the virtual view from which to 1039 * clear keyboard focus 1040 * @return whether this virtual view actually cleared keyboard focus 1041 */ 1042 public final boolean clearKeyboardFocusForVirtualView(int virtualViewId) { 1043 if (mKeyboardFocusedVirtualViewId != virtualViewId) { 1044 // The virtual view is not focused. 1045 return false; 1046 } 1047 1048 mKeyboardFocusedVirtualViewId = INVALID_ID; 1049 1050 onVirtualViewKeyboardFocusChanged(virtualViewId, false); 1051 sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_FOCUSED); 1052 1053 return true; 1054 } 1055 1056 /** 1057 * Provides a mapping between view-relative coordinates and logical 1058 * items. 1059 * 1060 * @param x The view-relative x coordinate 1061 * @param y The view-relative y coordinate 1062 * @return virtual view identifier for the logical item under 1063 * coordinates (x,y) or {@link #HOST_ID} if there is no item at 1064 * the given coordinates 1065 */ 1066 protected abstract int getVirtualViewAt(float x, float y); 1067 1068 /** 1069 * Populates a list with the view's visible items. The ordering of items 1070 * within {@code virtualViewIds} specifies order of accessibility focus 1071 * traversal. 1072 * 1073 * @param virtualViewIds The list to populate with visible items 1074 */ 1075 protected abstract void getVisibleVirtualViews(List<Integer> virtualViewIds); 1076 1077 /** 1078 * Populates an {@link AccessibilityEvent} with information about the 1079 * specified item. 1080 * <p> 1081 * The helper class automatically populates the following fields based on 1082 * the values set by 1083 * {@link #onPopulateNodeForVirtualView(int, AccessibilityNodeInfoCompat)}, 1084 * but implementations may optionally override them: 1085 * <ul> 1086 * <li>event text, see {@link AccessibilityEvent#getText()} 1087 * <li>content description, see 1088 * {@link AccessibilityEvent#setContentDescription(CharSequence)} 1089 * <li>scrollability, see {@link AccessibilityEvent#setScrollable(boolean)} 1090 * <li>password state, see {@link AccessibilityEvent#setPassword(boolean)} 1091 * <li>enabled state, see {@link AccessibilityEvent#setEnabled(boolean)} 1092 * <li>checked state, see {@link AccessibilityEvent#setChecked(boolean)} 1093 * </ul> 1094 * <p> 1095 * The following required fields are automatically populated by the 1096 * helper class and may not be overridden: 1097 * <ul> 1098 * <li>item class name, set to the value used in 1099 * {@link #onPopulateNodeForVirtualView(int, AccessibilityNodeInfoCompat)} 1100 * <li>package name, set to the package of the host view's 1101 * {@link Context}, see {@link AccessibilityEvent#setPackageName} 1102 * <li>event source, set to the host view and virtual view identifier, 1103 * see {@link AccessibilityRecordCompat#setSource(AccessibilityRecord, View, int)} 1104 * </ul> 1105 * 1106 * @param virtualViewId The virtual view id for the item for which to 1107 * populate the event 1108 * @param event The event to populate 1109 */ 1110 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 1111 // Default implementation is no-op. 1112 } 1113 1114 /** 1115 * Populates an {@link AccessibilityEvent} with information about the host 1116 * view. 1117 * <p> 1118 * The default implementation is a no-op. 1119 * 1120 * @param event the event to populate with information about the host view 1121 */ 1122 protected void onPopulateEventForHost(AccessibilityEvent event) { 1123 // Default implementation is no-op. 1124 } 1125 1126 /** 1127 * Populates an {@link AccessibilityNodeInfoCompat} with information 1128 * about the specified item. 1129 * <p> 1130 * Implementations <strong>must</strong> populate the following required 1131 * fields: 1132 * <ul> 1133 * <li>event text, see 1134 * {@link AccessibilityNodeInfoCompat#setText(CharSequence)} or 1135 * {@link AccessibilityNodeInfoCompat#setContentDescription(CharSequence)} 1136 * <li>bounds in parent coordinates, see 1137 * {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)} 1138 * </ul> 1139 * <p> 1140 * The helper class automatically populates the following fields with 1141 * default values, but implementations may optionally override them: 1142 * <ul> 1143 * <li>enabled state, set to {@code true}, see 1144 * {@link AccessibilityNodeInfoCompat#setEnabled(boolean)} 1145 * <li>keyboard focusability, set to {@code true}, see 1146 * {@link AccessibilityNodeInfoCompat#setFocusable(boolean)} 1147 * <li>item class name, set to {@code android.view.View}, see 1148 * {@link AccessibilityNodeInfoCompat#setClassName(CharSequence)} 1149 * </ul> 1150 * <p> 1151 * The following required fields are automatically populated by the 1152 * helper class and may not be overridden: 1153 * <ul> 1154 * <li>package name, identical to the package name set by 1155 * {@link #onPopulateEventForVirtualView(int, AccessibilityEvent)}, see 1156 * {@link AccessibilityNodeInfoCompat#setPackageName} 1157 * <li>node source, identical to the event source set in 1158 * {@link #onPopulateEventForVirtualView(int, AccessibilityEvent)}, see 1159 * {@link AccessibilityNodeInfoCompat#setSource(View, int)} 1160 * <li>parent view, set to the host view, see 1161 * {@link AccessibilityNodeInfoCompat#setParent(View)} 1162 * <li>visibility, computed based on parent-relative bounds, see 1163 * {@link AccessibilityNodeInfoCompat#setVisibleToUser(boolean)} 1164 * <li>accessibility focus, computed based on internal helper state, see 1165 * {@link AccessibilityNodeInfoCompat#setAccessibilityFocused(boolean)} 1166 * <li>keyboard focus, computed based on internal helper state, see 1167 * {@link AccessibilityNodeInfoCompat#setFocused(boolean)} 1168 * <li>bounds in screen coordinates, computed based on host view bounds, 1169 * see {@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)} 1170 * </ul> 1171 * <p> 1172 * Additionally, the helper class automatically handles keyboard focus and 1173 * accessibility focus management by adding the appropriate 1174 * {@link AccessibilityNodeInfoCompat#ACTION_FOCUS}, 1175 * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_FOCUS}, 1176 * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS}, or 1177 * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS} 1178 * actions. Implementations must <strong>never</strong> manually add these 1179 * actions. 1180 * <p> 1181 * The helper class also automatically modifies parent- and 1182 * screen-relative bounds to reflect the portion of the item visible 1183 * within its parent. 1184 * 1185 * @param virtualViewId The virtual view identifier of the item for 1186 * which to populate the node 1187 * @param node The node to populate 1188 */ 1189 protected abstract void onPopulateNodeForVirtualView( 1190 int virtualViewId, AccessibilityNodeInfoCompat node); 1191 1192 /** 1193 * Populates an {@link AccessibilityNodeInfoCompat} with information 1194 * about the host view. 1195 * <p> 1196 * The default implementation is a no-op. 1197 * 1198 * @param node the node to populate with information about the host view 1199 */ 1200 protected void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) { 1201 // Default implementation is no-op. 1202 } 1203 1204 /** 1205 * Performs the specified accessibility action on the item associated 1206 * with the virtual view identifier. See 1207 * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)} for 1208 * more information. 1209 * <p> 1210 * Implementations <strong>must</strong> handle any actions added manually 1211 * in 1212 * {@link #onPopulateNodeForVirtualView(int, AccessibilityNodeInfoCompat)}. 1213 * <p> 1214 * The helper class automatically handles focus management resulting 1215 * from {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} 1216 * and 1217 * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS} 1218 * actions. 1219 * 1220 * @param virtualViewId The virtual view identifier of the item on which 1221 * to perform the action 1222 * @param action The accessibility action to perform 1223 * @param arguments (Optional) A bundle with additional arguments, or 1224 * null 1225 * @return true if the action was performed 1226 */ 1227 protected abstract boolean onPerformActionForVirtualView( 1228 int virtualViewId, int action, Bundle arguments); 1229 1230 /** 1231 * Exposes a virtual view hierarchy to the accessibility framework. 1232 */ 1233 private class MyNodeProvider extends AccessibilityNodeProviderCompat { 1234 MyNodeProvider() { 1235 } 1236 1237 @Override 1238 public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) { 1239 // The caller takes ownership of the node and is expected to 1240 // recycle it when done, so always return a copy. 1241 final AccessibilityNodeInfoCompat node = 1242 ExploreByTouchHelper.this.obtainAccessibilityNodeInfo(virtualViewId); 1243 return AccessibilityNodeInfoCompat.obtain(node); 1244 } 1245 1246 @Override 1247 public boolean performAction(int virtualViewId, int action, Bundle arguments) { 1248 return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments); 1249 } 1250 1251 @Override 1252 public AccessibilityNodeInfoCompat findFocus(int focusType) { 1253 int focusedId = (focusType == AccessibilityNodeInfoCompat.FOCUS_ACCESSIBILITY) ? 1254 mAccessibilityFocusedVirtualViewId : mKeyboardFocusedVirtualViewId; 1255 if (focusedId == INVALID_ID) { 1256 return null; 1257 } 1258 return createAccessibilityNodeInfo(focusedId); 1259 } 1260 } 1261} 1262