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