1 /* 2 * Copyright 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.recyclerview.selection; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY; 20 import static androidx.core.util.Preconditions.checkArgument; 21 22 import android.annotation.SuppressLint; 23 import android.content.Context; 24 import android.os.Bundle; 25 import android.os.Parcelable; 26 import android.util.Log; 27 import android.view.GestureDetector; 28 import android.view.HapticFeedbackConstants; 29 import android.view.InputDevice; 30 import android.view.MotionEvent; 31 32 import androidx.annotation.DrawableRes; 33 import androidx.annotation.RestrictTo; 34 import androidx.recyclerview.widget.RecyclerView; 35 import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; 36 import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; 37 38 import org.jspecify.annotations.NonNull; 39 import org.jspecify.annotations.Nullable; 40 41 import java.util.Set; 42 43 /** 44 * SelectionTracker provides support for managing a selection of items in a RecyclerView instance. 45 * 46 * <p> 47 * This class provides support for managing a "primary" set of selected items, 48 * in addition to a "provisional" set of selected items using conventional 49 * {@link java.util.Collections}-like methods. 50 * 51 * <p> 52 * Create an instance of SelectionTracker using {@link Builder SelectionTracker.Builder}. 53 * 54 * <p> 55 * <b>Inspecting the current selection</b> 56 * 57 * <p> 58 * The underlying selection is described by the {@link Selection} class. 59 * 60 * <p> 61 * A live view of the current selection can be obtained using {@link #getSelection}. Changes made 62 * to the selection using SelectionTracker will be immediately reflected in this instance. 63 * 64 * <p> 65 * To obtain a stable snapshot of the selection use {@link #copySelection(MutableSelection)}. 66 * 67 * <p> 68 * Selection state for an individual item can be obtained using {@link #isSelected(Object)}. 69 * 70 * <p> 71 * <b>Provisional Selection</b> 72 * 73 * <p> 74 * Provisional selection exists to address issues where a transitory selection might 75 * momentarily intersect with a previously established selection resulting in a some 76 * or all of the established selection being erased. Such situations may arise 77 * when band selection is being performed in "additive" mode (e.g. SHIFT or CTRL is pressed 78 * on the keyboard prior to mouse down), or when there's an active gesture selection 79 * (which can be initiated by long pressing an unselected item while there is an 80 * existing selection). 81 * 82 * <p> 83 * A provisional selection can be abandoned, or merged into the primary selection. 84 * 85 * <p> 86 * <b>Enforcing selection policies</b> 87 * 88 * <p> 89 * Which items can be selected by the user is a matter of policy in an Application. 90 * Developers supply these policies by way of {@link SelectionPredicate}. 91 * 92 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types. 93 */ 94 public abstract class SelectionTracker<K> { 95 96 /** 97 * This value is included in the payload when SelectionTracker notifies RecyclerView 98 * of changes to selection. Look for this value in the {@code payload} 99 * Object argument supplied to 100 * {@link RecyclerView.Adapter#onBindViewHolder 101 * Adapter#onBindViewHolder}. 102 * If present the call is occurring in response to a selection state change. 103 * This would be a good opportunity to animate changes between unselected and selected state. 104 * When state is being restored, this argument will not be present. 105 */ 106 public static final String SELECTION_CHANGED_MARKER = "Selection-Changed"; 107 private static final String TAG = "SelectionTracker"; 108 109 /** 110 * Adds {@code observer} to be notified when changes to selection occur. 111 * 112 * <p> 113 * Use an observer to track attributes about the selection and 114 * update the UI to reflect the state of the selection. For example, an author 115 * may use an observer to control the enabled status of menu items, 116 * or to initiate {@link android.view.ActionMode}. 117 */ addObserver(@onNull SelectionObserver<K> observer)118 public abstract void addObserver(@NonNull SelectionObserver<K> observer); 119 120 /** @return true if has a selection */ hasSelection()121 public abstract boolean hasSelection(); 122 123 /** 124 * Returns a Selection object that provides a live view on the current selection. 125 * 126 * @return The current selection. 127 * @see #copySelection(MutableSelection) on how to get a snapshot 128 * of the selection that will not reflect future changes 129 * to selection. 130 */ getSelection()131 public abstract @NonNull Selection<K> getSelection(); 132 133 /** 134 * Updates {@code dest} to reflect the current selection. 135 */ copySelection(@onNull MutableSelection<K> dest)136 public abstract void copySelection(@NonNull MutableSelection<K> dest); 137 138 /** 139 * @return true if the item specified by its id is selected. Shorthand for 140 * {@code getSelection().contains(K)}. 141 */ isSelected(@ullable K key)142 public abstract boolean isSelected(@Nullable K key); 143 144 /** 145 * Restores the selected state of specified items. Used in cases such as restore the selection 146 * after rotation etc. Provisional selection is not restored. 147 * 148 * <p> 149 * This affords clients the ability to restore selection from selection saved 150 * in Activity state. 151 * 152 * @param selection selection being restored. 153 * @see StorageStrategy details on selection state support. 154 */ restoreSelection(@onNull Selection<K> selection)155 protected abstract void restoreSelection(@NonNull Selection<K> selection); 156 157 /** 158 * Clears both primary and provisional selections. 159 * 160 * @return true if primary selection changed. 161 */ clearSelection()162 public abstract boolean clearSelection(); 163 164 /** 165 * Sets the selected state of the specified items if permitted after consulting 166 * SelectionPredicate. 167 */ 168 @SuppressLint("LambdaLast") setItemsSelected(@onNull Iterable<K> keys, boolean selected)169 public abstract boolean setItemsSelected(@NonNull Iterable<K> keys, boolean selected); 170 171 /** 172 * Attempts to select an item. 173 * 174 * @return true if the item was selected. False if the item could not be selected, or was 175 * was already selected. 176 */ select(@onNull K key)177 public abstract boolean select(@NonNull K key); 178 179 /** 180 * Attempts to deselect an item. 181 * 182 * @return true if the item was deselected. False if the item could not be deselected, or was 183 * was already un-selected. 184 */ deselect(@onNull K key)185 public abstract boolean deselect(@NonNull K key); 186 187 @SuppressWarnings("HiddenAbstractMethod") 188 @RestrictTo(LIBRARY) getAdapterDataObserver()189 protected abstract @NonNull AdapterDataObserver getAdapterDataObserver(); 190 191 /** 192 * Attempts to establish a range selection at {@code position}, selecting the item 193 * at {@code position} if needed. 194 * 195 * @param position The "anchor" position for the range. Subsequent range operations 196 * (primarily keyboard and mouse based operations like SHIFT + click) 197 * work with the established anchor point to define selection ranges. 198 */ 199 @SuppressWarnings("HiddenAbstractMethod") 200 @RestrictTo(LIBRARY) startRange(int position)201 public abstract void startRange(int position); 202 203 /** 204 * Sets the end point for the active range selection. 205 * 206 * <p> 207 * This function should only be called when a range selection is active 208 * (see {@link #isRangeActive()}. Items in the range [anchor, end] will be 209 * selected after consulting SelectionPredicate. 210 * 211 * @param position The new end position for the selection range. 212 * @throws IllegalStateException if a range selection is not active. Range selection 213 * must have been started by a call to {@link #startRange(int)}. 214 */ 215 @SuppressWarnings("HiddenAbstractMethod") 216 @RestrictTo(LIBRARY) extendRange(int position)217 public abstract void extendRange(int position); 218 219 /** 220 * Clears an in-progress range selection. Provisional range selection established 221 * using {@link #extendProvisionalRange(int)} will be cleared (unless 222 * {@link #mergeProvisionalSelection()} is called first.) 223 */ 224 @SuppressWarnings("HiddenAbstractMethod") 225 @RestrictTo(LIBRARY) endRange()226 public abstract void endRange(); 227 228 /** 229 * @return Whether or not there is a current range selection active. 230 */ 231 @SuppressWarnings("HiddenAbstractMethod") 232 @RestrictTo(LIBRARY) isRangeActive()233 public abstract boolean isRangeActive(); 234 235 /** 236 * Establishes the "anchor" at which a selection range begins. This "anchor" is consulted 237 * when determining how to extend, and modify selection ranges. Calling this when a 238 * range selection is active will reset the range selection. 239 * 240 * TODO: Reconcile this with startRange. Maybe just docs need to be updated. 241 * 242 * @param position the anchor position. Must already be selected. 243 */ 244 @SuppressWarnings("HiddenAbstractMethod") 245 @RestrictTo(LIBRARY) anchorRange(int position)246 public abstract void anchorRange(int position); 247 248 /** 249 * Creates a provisional selection from anchor to {@code position}. 250 * 251 * @param position the end point. 252 */ 253 @SuppressWarnings("HiddenAbstractMethod") 254 @RestrictTo(LIBRARY) extendProvisionalRange(int position)255 protected abstract void extendProvisionalRange(int position); 256 257 /** 258 * Sets the provisional selection, replacing any existing selection. 259 */ 260 @SuppressWarnings("HiddenAbstractMethod") 261 @RestrictTo(LIBRARY) setProvisionalSelection(@onNull Set<K> newSelection)262 protected abstract void setProvisionalSelection(@NonNull Set<K> newSelection); 263 264 /** 265 * Clears any existing provisional selection 266 */ 267 @SuppressWarnings("HiddenAbstractMethod") 268 @RestrictTo(LIBRARY) clearProvisionalSelection()269 protected abstract void clearProvisionalSelection(); 270 271 /** 272 * Converts the provisional selection into primary selection, then clears 273 * provisional selection. 274 */ 275 @SuppressWarnings("HiddenAbstractMethod") 276 @RestrictTo(LIBRARY) mergeProvisionalSelection()277 protected abstract void mergeProvisionalSelection(); 278 279 /** 280 * Preserves selection, if any. Call this method from Activity#onSaveInstanceState 281 * 282 * @param state Bundle instance supplied to onSaveInstanceState. 283 */ onSaveInstanceState(@onNull Bundle state)284 public abstract void onSaveInstanceState(@NonNull Bundle state); 285 286 /** 287 * Restores selection from previously saved state. Call this method from 288 * Activity#onCreate. 289 * 290 * @param state Bundle instance supplied to onCreate. 291 */ onRestoreInstanceState(@ullable Bundle state)292 public abstract void onRestoreInstanceState(@Nullable Bundle state); 293 294 /** 295 * Observer class providing access to information about Selection state changes. 296 * 297 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types. 298 */ 299 public abstract static class SelectionObserver<K> { 300 301 /** 302 * Called when the state of an item has been changed. 303 */ onItemStateChanged(@onNull K key, boolean selected)304 public void onItemStateChanged(@NonNull K key, boolean selected) { 305 } 306 307 /** 308 * Called when Selection is cleared. 309 * TODO(smckay): Make public in a future public API. 310 */ 311 @RestrictTo(LIBRARY) onSelectionCleared()312 protected void onSelectionCleared() { 313 } 314 315 /** 316 * Called when the underlying data set has changed. After this method is called 317 * SelectionTracker will traverse the existing selection, 318 * calling {@link #onItemStateChanged(K, boolean)} for each selected item, 319 * and deselecting any items that cannot be selected given the updated data-set 320 * (and after consulting SelectionPredicate). 321 */ onSelectionRefresh()322 public void onSelectionRefresh() { 323 } 324 325 /** 326 * Called immediately after completion of any set of changes, excluding 327 * those resulting in calls {@link #onSelectionRestored()}. 328 */ onSelectionChanged()329 public void onSelectionChanged() { 330 } 331 332 /** 333 * Called immediately after selection is restored. 334 * {@link #onItemStateChanged(K, boolean)} will *not* be called 335 * for individual items in the selection. 336 */ onSelectionRestored()337 public void onSelectionRestored() { 338 } 339 } 340 341 /** 342 * Implement SelectionPredicate to control when items can be selected or unselected. 343 * See {@link Builder#withSelectionPredicate(SelectionPredicate)}. 344 * 345 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types. 346 */ 347 public abstract static class SelectionPredicate<K> { 348 349 /** 350 * Validates a change to selection for a specific key. 351 * 352 * @param key the item key 353 * @param nextState the next potential selected/unselected state 354 * @return true if the item at {@code id} can be set to {@code nextState}. 355 */ canSetStateForKey(@onNull K key, boolean nextState)356 public abstract boolean canSetStateForKey(@NonNull K key, boolean nextState); 357 358 /** 359 * Validates a change to selection for a specific position. If necessary 360 * use {@link ItemKeyProvider} to identy associated key. 361 * 362 * @param position the item position 363 * @param nextState the next potential selected/unselected state 364 * @return true if the item at {@code id} can be set to {@code nextState}. 365 */ canSetStateAtPosition(int position, boolean nextState)366 public abstract boolean canSetStateAtPosition(int position, boolean nextState); 367 368 /** 369 * Permits restriction to single selection mode. Single selection mode has 370 * unique behaviors in that it'll deselect an item already selected 371 * in order to select the new item. 372 * 373 * <p> 374 * In order to limit the number of items that can be selected, 375 * use {@link #canSetStateForKey(Object, boolean)} and 376 * {@link #canSetStateAtPosition(int, boolean)}. 377 * 378 * @return true if more than a single item can be selected. 379 */ canSelectMultiple()380 public abstract boolean canSelectMultiple(); 381 } 382 383 /** 384 * Builder is the primary mechanism for creating a {@link SelectionTracker} that 385 * can be used with your RecyclerView. Once installed, users will be able to create and 386 * manipulate a selection of items in a RecyclerView instance using a variety of 387 * intuitive techniques like tap, gesture, and mouse-based band selection (aka 'lasso'). 388 * 389 * <p> 390 * Building a bare-bones instance: 391 * 392 * <pre>{@code 393 * SelectionTracker<Uri> tracker = new SelectionTracker.Builder<>( 394 * "my-uri-selection", 395 * recyclerView, 396 * new YourItemKeyProvider(recyclerView.getAdapter()), 397 * new YourItemDetailsLookup(recyclerView), 398 * StorageStrategy.createParcelableStorage(Uri.class)) 399 * .build(); 400 * }</pre> 401 * 402 * <p> 403 * <b>Restricting which items can be selected and limiting selection size</b> 404 * 405 * <p> 406 * {@link SelectionPredicate} and 407 * {@link SelectionTracker.Builder#withSelectionPredicate(SelectionPredicate)} 408 * together provide a mechanism for restricting which items can be selected and 409 * limiting selection size. Use {@link SelectionPredicates#createSelectSingleAnything()} 410 * for single-selection, or write your own {@link SelectionPredicate} if other 411 * constraints are required. 412 * 413 * <pre>{@code 414 * SelectionTracker<String> tracker = new SelectionTracker.Builder<>( 415 * "my-string-selection", 416 * recyclerView, 417 * new YourItemKeyProvider(recyclerView.getAdapter()), 418 * new YourItemDetailsLookup(recyclerView), 419 * StorageStrategy.createStringStorage()) 420 * .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything()) 421 * .build(); 422 * }</pre> 423 * 424 * <p> 425 * <b>Retaining state across Android lifecycle events</b> 426 * 427 * <p> 428 * Support for storage/persistence of selection must be configured and invoked manually 429 * owing to its reliance on Activity lifecycle events. 430 * Failure to include support for selection storage will result in selection 431 * being lost when the Activity receives a configuration change (e.g. rotation), 432 * or when the application is paused or stopped. For this reason 433 * {@link StorageStrategy} is a required argument to obtain a {@link Builder} 434 * instance. 435 * 436 * <p> 437 * <b>Key Type</b> 438 * 439 * <p> 440 * A developer must decide on the key type used to identify selected items. 441 * Support is provided for three types: {@link Parcelable}, {@link String}, and {@link Long}. 442 * 443 * <p> 444 * {@link Parcelable}: Any Parcelable type can be used as the selection key. This is especially 445 * useful in conjunction with {@link android.net.Uri} as the Android URI implementation is both 446 * parcelable and makes for a natural stable selection key for values represented by 447 * the Android Content Provider framework. If items in your view are associated with 448 * stable {@code content://} uris, you should use Uri for your key type. 449 * 450 * <p> 451 * {@link String}: Use String when a string based stable identifier is available. 452 * 453 * <p> 454 * {@link Long}: Use Long when a project is already employing RecyclerView's built-in 455 * support for stable ids. In this case you may choose to use {@link StableIdKeyProvider} 456 * to supply selection keys to the SelectionTracker based on data already accessible 457 * in RecyclerView and it's Adapter. 458 * 459 * See {@link StableIdKeyProvider} for important details and limitations (<i>and a suggestion 460 * that you might just want to write your own {@link ItemKeyProvider}. It's easy!</i>) 461 * See the "Gotchas" selection below for details on selection size limits. 462 * 463 * <p> 464 * Usage: 465 * 466 * <pre>{@code 467 * private SelectionTracker<Uri> tracker; 468 * 469 * public void onCreate(Bundle savedInstanceState) { 470 * if (savedInstanceState != null) { 471 * tracker.onRestoreInstanceState(savedInstanceState); 472 * } 473 * } 474 * 475 * protected void onSaveInstanceState(Bundle outState) { 476 * super.onSaveInstanceState(outState); 477 * tracker.onSaveInstanceState(outState); 478 * } 479 * }</pre> 480 * 481 * <p> 482 * <b>Gotchas</b> 483 * 484 * <p>TransactionTooLargeException: 485 * 486 * <p>Many factors affect the maximum number of items that can be persisted when the 487 * application is paused or stopped. Unfortunately that number is not deterministic as it 488 * depends on the size of the key type used for selection, the number of selected items, and 489 * external demand on system resources. For that reason it is best to use the smallest viable 490 * key type, and to enforce a limit on the number of items that can be selected. 491 * 492 * <p>Furthermore the inability to persist a selection during a lifecycle event will result 493 * in a android.os.{@link android.os.TransactionTooLargeException}. See 494 * http://issuetracker.google.com/168706011 for details. 495 * 496 * <p>ItemTouchHelper 497 * 498 * <p>When using {@link SelectionTracker} along side an 499 * {@link androidx.recyclerview.widget.ItemTouchHelper} with the same RecyclerView instance 500 * the SelectionTracker instance must be created and installed before the ItemTouchHelper. 501 * Failure to do so will result in unintended selections during item drag operations, and 502 * possibly other situations. 503 * 504 * @param <K> Selection key type. Built in support is provided for {@link String}, 505 * {@link Long}, and {@link Parcelable}. {@link StorageStrategy} 506 * provides factory methods for each type: 507 * {@link StorageStrategy#createStringStorage()}, 508 * {@link StorageStrategy#createParcelableStorage(Class)}, 509 * {@link StorageStrategy#createLongStorage()} 510 */ 511 public static final class Builder<K> { 512 513 final RecyclerView mRecyclerView; 514 private final RecyclerView.Adapter<?> mAdapter; 515 private final Context mContext; 516 private final String mSelectionId; 517 private final StorageStrategy<K> mStorage; 518 519 SelectionPredicate<K> mSelectionPredicate = 520 SelectionPredicates.createSelectAnything(); 521 private OperationMonitor mMonitor = new OperationMonitor(); 522 private ItemKeyProvider<K> mKeyProvider; 523 private ItemDetailsLookup<K> mDetailsLookup; 524 525 private FocusDelegate<K> mFocusDelegate = FocusDelegate.stub(); 526 527 private OnItemActivatedListener<K> mOnItemActivatedListener; 528 private OnDragInitiatedListener mOnDragInitiatedListener; 529 private OnContextClickListener mOnContextClickListener; 530 531 private BandPredicate mBandPredicate; 532 private int mBandOverlayId = R.drawable.selection_band_overlay; 533 534 // TODO(b/144500333): Remove support for overriding gesture and pointer tooltypes. 535 private int[] mGestureToolTypes = new int[]{ 536 MotionEvent.TOOL_TYPE_FINGER 537 }; 538 539 private int[] mPointerToolTypes = new int[]{ 540 MotionEvent.TOOL_TYPE_MOUSE 541 }; 542 543 /** 544 * Creates a new SelectionTracker.Builder useful for configuring and creating 545 * a new SelectionTracker for use with your {@link RecyclerView}. 546 * 547 * @param selectionId A unique string identifying this selection in the context 548 * of the activity or fragment. 549 * @param recyclerView the owning RecyclerView 550 * @param keyProvider the source of selection keys 551 * @param detailsLookup the source of information about RecyclerView items. 552 * @param storage Strategy for type-safe storage of selection state in 553 * {@link Bundle}. 554 */ Builder( @onNull String selectionId, @NonNull RecyclerView recyclerView, @NonNull ItemKeyProvider<K> keyProvider, @NonNull ItemDetailsLookup<K> detailsLookup, @NonNull StorageStrategy<K> storage)555 public Builder( 556 @NonNull String selectionId, 557 @NonNull RecyclerView recyclerView, 558 @NonNull ItemKeyProvider<K> keyProvider, 559 @NonNull ItemDetailsLookup<K> detailsLookup, 560 @NonNull StorageStrategy<K> storage) { 561 562 checkArgument(selectionId != null); 563 checkArgument(!selectionId.trim().isEmpty()); 564 checkArgument(recyclerView != null); 565 566 mSelectionId = selectionId; 567 mRecyclerView = recyclerView; 568 mContext = recyclerView.getContext(); 569 mAdapter = recyclerView.getAdapter(); 570 571 checkArgument(mAdapter != null); 572 checkArgument(keyProvider != null); 573 checkArgument(detailsLookup != null); 574 checkArgument(storage != null); 575 576 mDetailsLookup = detailsLookup; 577 mKeyProvider = keyProvider; 578 mStorage = storage; 579 580 mBandPredicate = new BandPredicate.NonDraggableArea(mRecyclerView, detailsLookup); 581 } 582 583 /** 584 * Install selection predicate. 585 * 586 * @param predicate the predicate to be used. 587 * @return this 588 */ withSelectionPredicate( @onNull SelectionPredicate<K> predicate)589 public @NonNull Builder<K> withSelectionPredicate( 590 @NonNull SelectionPredicate<K> predicate) { 591 592 checkArgument(predicate != null); 593 mSelectionPredicate = predicate; 594 return this; 595 } 596 597 /** 598 * Add operation monitor allowing access to information about active 599 * operations (like band selection and gesture selection). 600 * 601 * @param monitor the monitor to be used 602 * @return this 603 */ withOperationMonitor( @onNull OperationMonitor monitor)604 public @NonNull Builder<K> withOperationMonitor( 605 @NonNull OperationMonitor monitor) { 606 607 checkArgument(monitor != null); 608 mMonitor = monitor; 609 return this; 610 } 611 612 /** 613 * Add focus delegate to interact with selection related focus changes. 614 * 615 * @param delegate the delegate to be used 616 * @return this 617 */ withFocusDelegate(@onNull FocusDelegate<K> delegate)618 public @NonNull Builder<K> withFocusDelegate(@NonNull FocusDelegate<K> delegate) { 619 checkArgument(delegate != null); 620 mFocusDelegate = delegate; 621 return this; 622 } 623 624 /** 625 * Adds an item activation listener. Respond to taps/enter/double-click on items. 626 * 627 * @param listener the listener to be used 628 * @return this 629 */ withOnItemActivatedListener( @onNull OnItemActivatedListener<K> listener)630 public @NonNull Builder<K> withOnItemActivatedListener( 631 @NonNull OnItemActivatedListener<K> listener) { 632 633 checkArgument(listener != null); 634 635 mOnItemActivatedListener = listener; 636 return this; 637 } 638 639 /** 640 * Adds a context click listener. Respond to right-click. 641 * 642 * @param listener the listener to be used 643 * @return this 644 */ withOnContextClickListener( @onNull OnContextClickListener listener)645 public @NonNull Builder<K> withOnContextClickListener( 646 @NonNull OnContextClickListener listener) { 647 648 checkArgument(listener != null); 649 650 mOnContextClickListener = listener; 651 return this; 652 } 653 654 /** 655 * Adds a drag initiated listener. Add support for drag and drop. 656 * 657 * @param listener the listener to be used 658 * @return this 659 */ withOnDragInitiatedListener( @onNull OnDragInitiatedListener listener)660 public @NonNull Builder<K> withOnDragInitiatedListener( 661 @NonNull OnDragInitiatedListener listener) { 662 663 checkArgument(listener != null); 664 665 mOnDragInitiatedListener = listener; 666 return this; 667 } 668 669 /** 670 * Replaces default tap and gesture tool-types. Defaults are: 671 * {@link MotionEvent#TOOL_TYPE_FINGER}. 672 * 673 * @param toolTypes the tool types to be used 674 * @return this 675 * @deprecated GestureSelection is best bound to {@link MotionEvent#TOOL_TYPE_FINGER}, 676 * and only that tool type. This method will be removed in a future release. 677 */ 678 @Deprecated withGestureTooltypes(int @NonNull ... toolTypes)679 public @NonNull Builder<K> withGestureTooltypes(int @NonNull ... toolTypes) { 680 Log.w(TAG, "Setting gestureTooltypes is likely to result in unexpected behavior."); 681 mGestureToolTypes = toolTypes; 682 return this; 683 } 684 685 /** 686 * Replaces default band overlay. 687 * 688 * @return this 689 */ withBandOverlay(@rawableRes int bandOverlayId)690 public @NonNull Builder<K> withBandOverlay(@DrawableRes int bandOverlayId) { 691 mBandOverlayId = bandOverlayId; 692 return this; 693 } 694 695 /** 696 * Replaces default band predicate. 697 * 698 * @return this 699 */ withBandPredicate(@onNull BandPredicate bandPredicate)700 public @NonNull Builder<K> withBandPredicate(@NonNull BandPredicate bandPredicate) { 701 mBandPredicate = bandPredicate; 702 return this; 703 } 704 705 /** 706 * Replaces default pointer tool-types. Pointer tools 707 * are associated with band selection, and certain 708 * drag and drop behaviors. Defaults are: 709 * {@link MotionEvent#TOOL_TYPE_MOUSE}. 710 * 711 * @param toolTypes the tool types to be used 712 * @return this 713 * @deprecated PointerSelection is best bound to {@link MotionEvent#TOOL_TYPE_MOUSE}, 714 * and only that tool type. This method will be removed in a future release. 715 */ 716 @Deprecated withPointerTooltypes(int @NonNull ... toolTypes)717 public @NonNull Builder<K> withPointerTooltypes(int @NonNull ... toolTypes) { 718 Log.w(TAG, "Setting pointerTooltypes is likely to result in unexpected behavior."); 719 mPointerToolTypes = toolTypes; 720 return this; 721 } 722 723 /** 724 * Prepares and returns a SelectionTracker. 725 * 726 * @return this 727 */ build()728 public @NonNull SelectionTracker<K> build() { 729 730 DefaultSelectionTracker<K> tracker = new DefaultSelectionTracker<>( 731 mSelectionId, mKeyProvider, mSelectionPredicate, mStorage); 732 733 // Event glue between RecyclerView and SelectionTracker keeps the classes separate 734 // so that a SelectionTracker can be shared across RecyclerView instances that 735 // represent the same data in different ways. 736 EventBridge.install(mAdapter, tracker, mKeyProvider, mRecyclerView::post); 737 738 // Scroller is stateful and can be reset, but we don't manage it directly. 739 // GestureSelectionHelper will reset scroller when it is reset. 740 AutoScroller scroller = 741 new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecyclerView)); 742 743 // Setup basic input handling, with the touch handler as the default consumer 744 // of events. If mouse handling is configured as well, the mouse input 745 // related handlers will intercept mouse input events. 746 747 // GestureRouter is responsible for routing GestureDetector events 748 // to tool-type specific handlers. 749 GestureRouter<MotionInputHandler<K>> gestureRouter = new GestureRouter<>(); 750 751 // GestureDetector cancels itself in response to ACTION_CANCEL events. 752 GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter); 753 754 // GestureSelectionHelper provides logic that interprets a combination 755 // of motions and gestures in order to provide fluid "long-press and drag" 756 // finger driven selection support. 757 final GestureSelectionHelper gestureSelectionHelper = GestureSelectionHelper.create( 758 tracker, mSelectionPredicate, mRecyclerView, scroller, mMonitor); 759 760 // EventRouter receives events for RecyclerView, dispatching to handlers 761 // registered by tool-type. 762 EventRouter eventRouter = new EventRouter(); 763 GestureDetectorWrapper gestureDetectorWrapper = 764 new GestureDetectorWrapper(gestureDetector); 765 766 // Temp fix for b/166836317. 767 // TODO: Add support for multiple listeners per tool type to EventRouter, then 768 // register backstop with primary router. 769 EventRouter backstopRouter = new EventRouter(); 770 EventBackstop backstop = new EventBackstop(); 771 DisallowInterceptFilter backstopWrapper = new DisallowInterceptFilter(backstop); 772 backstopRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_UNKNOWN), backstopWrapper); 773 774 // Finally hook the framework up to listening to RecycleView events. 775 mRecyclerView.addOnItemTouchListener(eventRouter); 776 mRecyclerView.addOnItemTouchListener(gestureDetectorWrapper); 777 mRecyclerView.addOnItemTouchListener(backstopRouter); 778 779 // Reset manager listens for cancel events from RecyclerView. In response to that it 780 // advises other classes it is time to reset state. 781 ResetManager<K> resetMgr = new ResetManager<>(); 782 783 // Register ResetManager to: 784 // 785 // 1. Monitor selection reset which can be invoked by clients in response 786 // to back key press and some application lifecycle events. 787 tracker.addObserver(resetMgr.getSelectionObserver()); 788 789 // ...and 2. Monitor ACTION_CANCEL events (which arrive exclusively 790 // via TOOL_TYPE_UNKNOWN). 791 // 792 // CAUTION! Registering resetMgr directly with RecyclerView#addOnItemTouchListener 793 // will not work as expected. Once EventRouter returns true, RecyclerView will 794 // no longer dispatch any events to other listeners for the duration of the 795 // stream, not even ACTION_CANCEL events. 796 eventRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_UNKNOWN), 797 resetMgr.getInputListener()); 798 799 // Finally register all of the Resettables. 800 resetMgr.addResetHandler(tracker); 801 resetMgr.addResetHandler(mMonitor.asResettable()); 802 resetMgr.addResetHandler(gestureSelectionHelper); 803 resetMgr.addResetHandler(gestureDetectorWrapper); 804 resetMgr.addResetHandler(eventRouter); 805 resetMgr.addResetHandler(backstopRouter); 806 resetMgr.addResetHandler(backstop); 807 resetMgr.addResetHandler(backstopWrapper); 808 809 // But before you move on, there's more work to do. Event plumbing has been 810 // installed, but we haven't registered any of our helpers or callbacks. 811 // Helpers contain predefined logic converting events into selection related events. 812 // Callbacks provide developers the ability to reponspond to other types of 813 // events (like "activate" a tapped item). This is broken up into two main 814 // suites, one for "touch" and one for "mouse", though both can and should (usually) 815 // be configured to handle other types of input (to satisfy user expectation).); 816 817 // Internally, the code doesn't permit nullable listeners, so we lazily 818 // initialize stub instances if the developer didn't supply a real listener. 819 mOnDragInitiatedListener = (mOnDragInitiatedListener != null) 820 ? mOnDragInitiatedListener 821 : new OnDragInitiatedListener() { 822 @Override 823 public boolean onDragInitiated(@NonNull MotionEvent e) { 824 return false; 825 } 826 }; 827 828 mOnItemActivatedListener = (mOnItemActivatedListener != null) 829 ? mOnItemActivatedListener 830 : new OnItemActivatedListener<K>() { 831 @Override 832 public boolean onItemActivated( 833 ItemDetailsLookup.@NonNull ItemDetails<K> item, 834 @NonNull MotionEvent e) { 835 return false; 836 } 837 }; 838 839 mOnContextClickListener = (mOnContextClickListener != null) 840 ? mOnContextClickListener 841 : new OnContextClickListener() { 842 @Override 843 public boolean onContextClick(@NonNull MotionEvent e) { 844 return false; 845 } 846 }; 847 848 // Provides high level glue for binding touch events 849 // and gestures to selection framework. 850 TouchInputHandler<K> touchHandler = new TouchInputHandler<>( 851 tracker, 852 mKeyProvider, 853 mDetailsLookup, 854 mSelectionPredicate, 855 gestureSelectionHelper::start, 856 mOnDragInitiatedListener, 857 mOnItemActivatedListener, 858 mFocusDelegate, 859 new Runnable() { 860 @Override 861 public void run() { 862 mRecyclerView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 863 } 864 }, 865 // Provide temporary glue to address b/166836317 866 backstop::onLongPress); 867 868 for (int toolType : mGestureToolTypes) { 869 ToolSourceKey key = new ToolSourceKey(toolType); 870 gestureRouter.register(key, touchHandler); 871 eventRouter.set(key, gestureSelectionHelper); 872 } 873 874 // Provides high level glue for binding mouse events and gestures 875 // to selection framework. 876 MouseInputHandler<K> mouseHandler = new MouseInputHandler<>( 877 tracker, 878 mKeyProvider, 879 mDetailsLookup, 880 mOnContextClickListener, 881 mOnItemActivatedListener, 882 mFocusDelegate); 883 884 for (int toolType : mPointerToolTypes) { 885 gestureRouter.register(new ToolSourceKey(toolType), mouseHandler); 886 } 887 888 ToolSourceKey touchpadKey = new ToolSourceKey(MotionEvent.TOOL_TYPE_FINGER, 889 InputDevice.SOURCE_MOUSE); 890 gestureRouter.register(touchpadKey, mouseHandler); 891 892 BandSelectionHelper<K> bandHelper = null; 893 894 // Band selection not supported in single select mode, or when key access 895 // is limited to anything less than the entire corpus. 896 if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED) 897 && mSelectionPredicate.canSelectMultiple()) { 898 // BandSelectionHelper provides support for band selection on-top of a RecyclerView 899 // instance. Given the recycling nature of RecyclerView BandSelectionController 900 // necessarily models and caches list/grid information as the user's pointer 901 // interacts with the item in the RecyclerView. Selectable items that intersect 902 // with the band, both on and off screen, are selected. 903 bandHelper = BandSelectionHelper.create( 904 mRecyclerView, 905 scroller, 906 mBandOverlayId, 907 mKeyProvider, 908 tracker, 909 mSelectionPredicate, 910 mBandPredicate, 911 mFocusDelegate, 912 mMonitor); 913 914 resetMgr.addResetHandler(bandHelper); 915 } 916 917 OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor( 918 mDetailsLookup, mOnDragInitiatedListener, bandHelper); 919 920 eventRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_MOUSE), pointerEventHandler); 921 eventRouter.set(touchpadKey, pointerEventHandler); 922 923 return tracker; 924 } 925 } 926 } 927