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