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