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