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.core.util.Preconditions.checkArgument; 20 21 import android.graphics.Point; 22 import android.graphics.Rect; 23 import android.util.Log; 24 import android.util.SparseArray; 25 import android.util.SparseBooleanArray; 26 import android.util.SparseIntArray; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; 31 import androidx.recyclerview.widget.RecyclerView; 32 import androidx.recyclerview.widget.RecyclerView.OnScrollListener; 33 34 import java.util.ArrayList; 35 import java.util.Collections; 36 import java.util.HashSet; 37 import java.util.List; 38 import java.util.Set; 39 40 /** 41 * Provides a band selection item model for views within a RecyclerView. This class queries the 42 * RecyclerView to determine where its items are placed; then, once band selection is underway, 43 * it alerts listeners of which items are covered by the selections. 44 * 45 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types. 46 */ 47 final class GridModel<K> { 48 49 // Magical value indicating that a value has not been previously set. primitive null :) 50 static final int NOT_SET = -1; 51 52 // Enum values used to determine the corner at which the origin is located within the 53 private static final int UPPER = 0x00; 54 private static final int LOWER = 0x01; 55 private static final int LEFT = 0x00; 56 private static final int RIGHT = 0x02; 57 private static final int UPPER_LEFT = UPPER | LEFT; 58 private static final int UPPER_RIGHT = UPPER | RIGHT; 59 private static final int LOWER_LEFT = LOWER | LEFT; 60 private static final int LOWER_RIGHT = LOWER | RIGHT; 61 62 private final GridHost<K> mHost; 63 private final ItemKeyProvider<K> mKeyProvider; 64 private final SelectionPredicate<K> mSelectionPredicate; 65 66 private final List<SelectionObserver> mOnSelectionChangedListeners = new ArrayList<>(); 67 68 // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed 69 // by their y-offset. For example, if the first column of the view starts at an x-value of 5, 70 // mColumns.get(5) would return an array of positions in that column. Within that array, the 71 // value for key y is the adapter position for the item whose y-offset is y. 72 private final SparseArray<SparseIntArray> mColumns = new SparseArray<>(); 73 74 // List of limits along the x-axis (columns). 75 // This list is sorted from furthest left to furthest right. 76 private final List<Limits> mColumnBounds = new ArrayList<>(); 77 78 // List of limits along the y-axis (rows). Note that this list only contains items which 79 // have been in the viewport. 80 private final List<Limits> mRowBounds = new ArrayList<>(); 81 82 // The adapter positions which have been recorded so far. 83 private final SparseBooleanArray mKnownPositions = new SparseBooleanArray(); 84 85 // Array passed to registered OnSelectionChangedListeners. One array is created and reused 86 // throughout the lifetime of the object. 87 private final Set<K> mSelection = new HashSet<>(); 88 89 // The current pointer (in absolute positioning from the top of the view). 90 private Point mPointer; 91 92 // The bounds of the band selection. 93 private RelativePoint mRelOrigin; 94 private RelativePoint mRelPointer; 95 96 private boolean mIsActive; 97 98 // Tracks where the band select originated from. This is used to determine where selections 99 // should expand from when Shift+click is used. 100 private int mPositionNearestOrigin = NOT_SET; 101 102 private final OnScrollListener mScrollListener; 103 104 GridModel( 105 GridHost host, 106 ItemKeyProvider<K> keyProvider, 107 SelectionPredicate<K> selectionPredicate) { 108 109 checkArgument(host != null); 110 checkArgument(keyProvider != null); 111 checkArgument(selectionPredicate != null); 112 113 mHost = host; 114 mKeyProvider = keyProvider; 115 mSelectionPredicate = selectionPredicate; 116 117 mScrollListener = new OnScrollListener() { 118 @Override 119 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 120 GridModel.this.onScrolled(recyclerView, dx, dy); 121 } 122 }; 123 124 mHost.addOnScrollListener(mScrollListener); 125 } 126 127 /** 128 * Start a band select operation at the given point. 129 * 130 * @param relativeOrigin The origin of the band select operation, relative to the viewport. 131 * For example, if the view is scrolled to the bottom, the top-left of 132 * the 133 * viewport 134 * would have a relative origin of (0, 0), even though its absolute point 135 * has a higher 136 * y-value. 137 */ 138 void startCapturing(Point relativeOrigin) { 139 recordVisibleChildren(); 140 if (isEmpty()) { 141 // The selection band logic works only if there is at least one visible child. 142 return; 143 } 144 145 mIsActive = true; 146 mPointer = mHost.createAbsolutePoint(relativeOrigin); 147 mRelOrigin = createRelativePoint(mPointer); 148 mRelPointer = createRelativePoint(mPointer); 149 computeCurrentSelection(); 150 notifySelectionChanged(); 151 } 152 153 /** 154 * Ends the band selection. 155 */ 156 void stopCapturing() { 157 mIsActive = false; 158 } 159 160 /** 161 * Resizes the selection by adjusting the pointer (i.e., the corner of the selection 162 * opposite the origin. 163 * 164 * @param relativePointer The pointer (opposite of the origin) of the band select operation, 165 * relative to the viewport. For example, if the view is scrolled to the 166 * bottom, the 167 * top-left of the viewport would have a relative origin of (0, 0), even 168 * though its 169 * absolute point has a higher y-value. 170 */ 171 void resizeSelection(Point relativePointer) { 172 mPointer = mHost.createAbsolutePoint(relativePointer); 173 updateModel(); 174 } 175 176 /** 177 * @return The adapter position for the item nearest the origin corresponding to the latest 178 * band select operation, or NOT_SET if the selection did not cover any items. 179 */ 180 int getPositionNearestOrigin() { 181 return mPositionNearestOrigin; 182 } 183 184 private void onScrolled(RecyclerView recyclerView, int dx, int dy) { 185 if (!mIsActive) { 186 return; 187 } 188 189 mPointer.x += dx; 190 mPointer.y += dy; 191 recordVisibleChildren(); 192 updateModel(); 193 } 194 195 /** 196 * Queries the view for all children and records their location metadata. 197 */ 198 private void recordVisibleChildren() { 199 for (int i = 0; i < mHost.getVisibleChildCount(); i++) { 200 int adapterPosition = mHost.getAdapterPositionAt(i); 201 // Sometimes the view is not attached, as we notify the multi selection manager 202 // synchronously, while views are attached asynchronously. As a result items which 203 // are in the adapter may not actually have a corresponding view (yet). 204 if (mHost.hasView(adapterPosition) 205 && mSelectionPredicate.canSetStateAtPosition(adapterPosition, true) 206 && !mKnownPositions.get(adapterPosition)) { 207 mKnownPositions.put(adapterPosition, true); 208 recordItemData(mHost.getAbsoluteRectForChildViewAt(i), adapterPosition); 209 } 210 } 211 } 212 213 /** 214 * Checks if there are any recorded children. 215 */ 216 private boolean isEmpty() { 217 return mColumnBounds.size() == 0 || mRowBounds.size() == 0; 218 } 219 220 /** 221 * Updates the limits lists and column map with the given item metadata. 222 * 223 * @param absoluteChildRect The absolute rectangle for the child view being processed. 224 * @param adapterPosition The position of the child view being processed. 225 */ 226 private void recordItemData(Rect absoluteChildRect, int adapterPosition) { 227 if (mColumnBounds.size() != mHost.getColumnCount()) { 228 // If not all x-limits have been recorded, record this one. 229 recordLimits( 230 mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right)); 231 } 232 233 recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom)); 234 235 SparseIntArray columnList = mColumns.get(absoluteChildRect.left); 236 if (columnList == null) { 237 columnList = new SparseIntArray(); 238 mColumns.put(absoluteChildRect.left, columnList); 239 } 240 columnList.put(absoluteChildRect.top, adapterPosition); 241 } 242 243 /** 244 * Ensures limits exists within the sorted list limitsList, and adds it to the list if it 245 * does not exist. 246 */ 247 private void recordLimits(List<Limits> limitsList, Limits limits) { 248 int index = Collections.binarySearch(limitsList, limits); 249 if (index < 0) { 250 limitsList.add(~index, limits); 251 } 252 } 253 254 /** 255 * Handles a moved pointer; this function determines whether the pointer movement resulted 256 * in a selection change and, if it has, notifies listeners of this change. 257 */ 258 private void updateModel() { 259 RelativePoint old = mRelPointer; 260 mRelPointer = createRelativePoint(mPointer); 261 if (old != null && mRelPointer.equals(old)) { 262 return; 263 } 264 265 computeCurrentSelection(); 266 notifySelectionChanged(); 267 } 268 269 /** 270 * Computes the currently-selected items. 271 */ 272 private void computeCurrentSelection() { 273 if (areItemsCoveredByBand(mRelPointer, mRelOrigin)) { 274 updateSelection(computeBounds()); 275 } else { 276 mSelection.clear(); 277 mPositionNearestOrigin = NOT_SET; 278 } 279 } 280 281 /** 282 * Notifies all listeners of a selection change. Note that this function simply passes 283 * mSelection, so computeCurrentSelection() should be called before this 284 * function. 285 */ 286 private void notifySelectionChanged() { 287 for (SelectionObserver listener : mOnSelectionChangedListeners) { 288 listener.onSelectionChanged(mSelection); 289 } 290 } 291 292 /** 293 * @param rect Rectangle including all covered items. 294 */ 295 private void updateSelection(Rect rect) { 296 int columnStart = 297 Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left)); 298 299 checkArgument(columnStart >= 0, "Rect doesn't intesect any known column."); 300 301 int columnEnd = columnStart; 302 303 for (int i = columnStart; i < mColumnBounds.size() 304 && mColumnBounds.get(i).lowerLimit <= rect.right; i++) { 305 columnEnd = i; 306 } 307 308 int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top)); 309 if (rowStart < 0) { 310 mPositionNearestOrigin = NOT_SET; 311 return; 312 } 313 314 int rowEnd = rowStart; 315 for (int i = rowStart; i < mRowBounds.size() 316 && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) { 317 rowEnd = i; 318 } 319 320 updateSelection(columnStart, columnEnd, rowStart, rowEnd); 321 } 322 323 /** 324 * Computes the selection given the previously-computed start- and end-indices for each 325 * row and column. 326 */ 327 private void updateSelection( 328 int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) { 329 330 if (BandSelectionHelper.DEBUG) { 331 Log.d(BandSelectionHelper.TAG, String.format( 332 "updateSelection: %d, %d, %d, %d", 333 columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex)); 334 } 335 336 mSelection.clear(); 337 for (int column = columnStartIndex; column <= columnEndIndex; column++) { 338 SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit); 339 for (int row = rowStartIndex; row <= rowEndIndex; row++) { 340 // The default return value for SparseIntArray.get is 0, which is a valid 341 // position. Use a sentry value to prevent erroneously selecting item 0. 342 final int rowKey = mRowBounds.get(row).lowerLimit; 343 int position = items.get(rowKey, NOT_SET); 344 if (position != NOT_SET) { 345 K key = mKeyProvider.getKey(position); 346 if (key != null) { 347 // The adapter inserts items for UI layout purposes that aren't 348 // associated with files. Those will have a null model ID. 349 // Don't select them. 350 if (canSelect(key)) { 351 mSelection.add(key); 352 } 353 } 354 if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex, 355 row, rowStartIndex, rowEndIndex)) { 356 // If this is the position nearest the origin, record it now so that it 357 // can be returned by endSelection() later. 358 mPositionNearestOrigin = position; 359 } 360 } 361 } 362 } 363 } 364 365 private boolean canSelect(K key) { 366 return mSelectionPredicate.canSetStateForKey(key, true); 367 } 368 369 /** 370 * @return Returns true if the position is the nearest to the origin, or, in the case of the 371 * lower-right corner, whether it is possible that the position is the nearest to the 372 * origin. See comment below for reasoning for this special case. 373 */ 374 private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex, 375 int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) { 376 int corner = computeCornerNearestOrigin(); 377 switch (corner) { 378 case UPPER_LEFT: 379 return columnIndex == columnStartIndex && rowIndex == rowStartIndex; 380 case UPPER_RIGHT: 381 return columnIndex == columnEndIndex && rowIndex == rowStartIndex; 382 case LOWER_LEFT: 383 return columnIndex == columnStartIndex && rowIndex == rowEndIndex; 384 case LOWER_RIGHT: 385 // Note that in some cases, the last row will not have as many items as there 386 // are columns (e.g., if there are 4 items and 3 columns, the second row will 387 // only have one item in the first column). This function is invoked for each 388 // position from left to right, so return true for any position in the bottom 389 // row and only the right-most position in the bottom row will be recorded. 390 return rowIndex == rowEndIndex; 391 default: 392 throw new RuntimeException("Invalid corner type."); 393 } 394 } 395 396 /** 397 * Listener for changes in which items have been band selected. 398 */ 399 public abstract static class SelectionObserver<K> { 400 abstract void onSelectionChanged(Set<K> updatedSelection); 401 } 402 403 void addOnSelectionChangedListener(SelectionObserver listener) { 404 mOnSelectionChangedListeners.add(listener); 405 } 406 407 /** 408 * Called when {@link BandSelectionHelper} is finished with a GridModel. 409 */ 410 void onDestroy() { 411 mOnSelectionChangedListeners.clear(); 412 // Cleanup listeners to prevent memory leaks. 413 mHost.removeOnScrollListener(mScrollListener); 414 } 415 416 /** 417 * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side 418 * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides 419 * of item columns and the top- and bottom sides of item rows so that it can be determined 420 * whether the pointer is located within the bounds of an item. 421 */ 422 private static class Limits implements Comparable<Limits> { 423 public int lowerLimit; 424 public int upperLimit; 425 426 Limits(int lowerLimit, int upperLimit) { 427 this.lowerLimit = lowerLimit; 428 this.upperLimit = upperLimit; 429 } 430 431 @Override 432 public int compareTo(Limits other) { 433 return lowerLimit - other.lowerLimit; 434 } 435 436 @Override 437 public int hashCode() { 438 return lowerLimit ^ upperLimit; 439 } 440 441 @Override 442 public boolean equals(Object other) { 443 if (!(other instanceof Limits)) { 444 return false; 445 } 446 447 return ((Limits) other).lowerLimit == lowerLimit 448 && ((Limits) other).upperLimit == upperLimit; 449 } 450 451 @Override 452 public String toString() { 453 return "(" + lowerLimit + ", " + upperLimit + ")"; 454 } 455 } 456 457 /** 458 * The location of a coordinate relative to items. This class represents a general area of the 459 * view as it relates to band selection rather than an explicit point. For example, two 460 * different points within an item are considered to have the same "location" because band 461 * selection originating within the item would select the same items no matter which point 462 * was used. Same goes for points between items as well as those at the very beginning or end 463 * of the view. 464 * 465 * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the 466 * advantage of tying the value to the Limits of items along that axis. This allows easy 467 * selection of items within those Limits as opposed to a search through every item to see if a 468 * given coordinate value falls within those Limits. 469 */ 470 private static class RelativeCoordinate 471 implements Comparable<RelativeCoordinate> { 472 /** 473 * Location describing points after the last known item. 474 */ 475 static final int AFTER_LAST_ITEM = 0; 476 477 /** 478 * Location describing points before the first known item. 479 */ 480 static final int BEFORE_FIRST_ITEM = 1; 481 482 /** 483 * Location describing points between two items. 484 */ 485 static final int BETWEEN_TWO_ITEMS = 2; 486 487 /** 488 * Location describing points within the limits of one item. 489 */ 490 static final int WITHIN_LIMITS = 3; 491 492 /** 493 * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM, 494 * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS. 495 */ 496 public final int type; 497 498 /** 499 * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type == 500 * BETWEEN_TWO_ITEMS. 501 */ 502 public Limits limitsBeforeCoordinate; 503 504 /** 505 * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS. 506 */ 507 public Limits limitsAfterCoordinate; 508 509 // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM. 510 public Limits mFirstKnownItem; 511 // Limits of the last known item; only populated when type == AFTER_LAST_ITEM. 512 public Limits mLastKnownItem; 513 514 /** 515 * @param limitsList The sorted limits list for the coordinate type. If this 516 * CoordinateLocation is an x-value, mXLimitsList should be passed; 517 * otherwise, 518 * mYLimitsList should be pased. 519 * @param value The coordinate value. 520 */ 521 RelativeCoordinate(List<Limits> limitsList, int value) { 522 int index = Collections.binarySearch(limitsList, new Limits(value, value)); 523 524 if (index >= 0) { 525 this.type = WITHIN_LIMITS; 526 this.limitsBeforeCoordinate = limitsList.get(index); 527 } else if (~index == 0) { 528 this.type = BEFORE_FIRST_ITEM; 529 this.mFirstKnownItem = limitsList.get(0); 530 } else if (~index == limitsList.size()) { 531 Limits lastLimits = limitsList.get(limitsList.size() - 1); 532 if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) { 533 this.type = WITHIN_LIMITS; 534 this.limitsBeforeCoordinate = lastLimits; 535 } else { 536 this.type = AFTER_LAST_ITEM; 537 this.mLastKnownItem = lastLimits; 538 } 539 } else { 540 Limits limitsBeforeIndex = limitsList.get(~index - 1); 541 if (limitsBeforeIndex.lowerLimit <= value 542 && value <= limitsBeforeIndex.upperLimit) { 543 this.type = WITHIN_LIMITS; 544 this.limitsBeforeCoordinate = limitsList.get(~index - 1); 545 } else { 546 this.type = BETWEEN_TWO_ITEMS; 547 this.limitsBeforeCoordinate = limitsList.get(~index - 1); 548 this.limitsAfterCoordinate = limitsList.get(~index); 549 } 550 } 551 } 552 553 int toComparisonValue() { 554 if (type == BEFORE_FIRST_ITEM) { 555 return mFirstKnownItem.lowerLimit - 1; 556 } else if (type == AFTER_LAST_ITEM) { 557 return mLastKnownItem.upperLimit + 1; 558 } else if (type == BETWEEN_TWO_ITEMS) { 559 return limitsBeforeCoordinate.upperLimit + 1; 560 } else { 561 return limitsBeforeCoordinate.lowerLimit; 562 } 563 } 564 565 @Override 566 public int hashCode() { 567 return mFirstKnownItem.lowerLimit 568 ^ mLastKnownItem.upperLimit 569 ^ limitsBeforeCoordinate.upperLimit 570 ^ limitsBeforeCoordinate.lowerLimit; 571 } 572 573 @Override 574 public boolean equals(Object other) { 575 if (!(other instanceof RelativeCoordinate)) { 576 return false; 577 } 578 579 RelativeCoordinate otherCoordinate = (RelativeCoordinate) other; 580 return toComparisonValue() == otherCoordinate.toComparisonValue(); 581 } 582 583 @Override 584 public int compareTo(RelativeCoordinate other) { 585 return toComparisonValue() - other.toComparisonValue(); 586 } 587 } 588 589 RelativePoint createRelativePoint(Point point) { 590 return new RelativePoint( 591 new RelativeCoordinate(mColumnBounds, point.x), 592 new RelativeCoordinate(mRowBounds, point.y)); 593 } 594 595 /** 596 * The location of a point relative to the Limits of nearby items; consists of both an x- and 597 * y-RelativeCoordinateLocation. 598 */ 599 private static class RelativePoint { 600 601 final RelativeCoordinate mX; 602 final RelativeCoordinate mY; 603 604 RelativePoint( 605 @NonNull List<Limits> columnLimits, 606 @NonNull List<Limits> rowLimits, Point point) { 607 608 this.mX = new RelativeCoordinate(columnLimits, point.x); 609 this.mY = new RelativeCoordinate(rowLimits, point.y); 610 } 611 612 RelativePoint(@NonNull RelativeCoordinate x, @NonNull RelativeCoordinate y) { 613 this.mX = x; 614 this.mY = y; 615 } 616 617 @Override 618 public int hashCode() { 619 return mX.toComparisonValue() ^ mY.toComparisonValue(); 620 } 621 622 @Override 623 public boolean equals(@Nullable Object other) { 624 if (!(other instanceof RelativePoint)) { 625 return false; 626 } 627 628 RelativePoint otherPoint = (RelativePoint) other; 629 return mX.equals(otherPoint.mX) && mY.equals(otherPoint.mY); 630 } 631 } 632 633 /** 634 * Generates a rectangle which contains the items selected by the pointer and origin. 635 * 636 * @return The rectangle, or null if no items were selected. 637 */ 638 private Rect computeBounds() { 639 Rect rect = new Rect(); 640 rect.left = getCoordinateValue( 641 min(mRelOrigin.mX, mRelPointer.mX), 642 mColumnBounds, 643 true); 644 rect.right = getCoordinateValue( 645 max(mRelOrigin.mX, mRelPointer.mX), 646 mColumnBounds, 647 false); 648 rect.top = getCoordinateValue( 649 min(mRelOrigin.mY, mRelPointer.mY), 650 mRowBounds, 651 true); 652 rect.bottom = getCoordinateValue( 653 max(mRelOrigin.mY, mRelPointer.mY), 654 mRowBounds, 655 false); 656 return rect; 657 } 658 659 /** 660 * Computes the corner of the selection nearest the origin. 661 */ 662 private int computeCornerNearestOrigin() { 663 int cornerValue = 0; 664 665 if (mRelOrigin.mY.equals(min(mRelOrigin.mY, mRelPointer.mY))) { 666 cornerValue |= UPPER; 667 } else { 668 cornerValue |= LOWER; 669 } 670 671 if (mRelOrigin.mX.equals(min(mRelOrigin.mX, mRelPointer.mX))) { 672 cornerValue |= LEFT; 673 } else { 674 cornerValue |= RIGHT; 675 } 676 677 return cornerValue; 678 } 679 680 private RelativeCoordinate min( 681 @NonNull RelativeCoordinate first, @NonNull RelativeCoordinate second) { 682 return first.compareTo(second) < 0 ? first : second; 683 } 684 685 private RelativeCoordinate max( 686 @NonNull RelativeCoordinate first, @NonNull RelativeCoordinate second) { 687 return first.compareTo(second) > 0 ? first : second; 688 } 689 690 /** 691 * @return The absolute coordinate (i.e., the x- or y-value) of the given relative 692 * coordinate. 693 */ 694 private int getCoordinateValue( 695 @NonNull RelativeCoordinate coordinate, 696 @NonNull List<Limits> limitsList, 697 boolean isStartOfRange) { 698 699 switch (coordinate.type) { 700 case RelativeCoordinate.BEFORE_FIRST_ITEM: 701 return limitsList.get(0).lowerLimit; 702 case RelativeCoordinate.AFTER_LAST_ITEM: 703 return limitsList.get(limitsList.size() - 1).upperLimit; 704 case RelativeCoordinate.BETWEEN_TWO_ITEMS: 705 if (isStartOfRange) { 706 return coordinate.limitsAfterCoordinate.lowerLimit; 707 } else { 708 return coordinate.limitsBeforeCoordinate.upperLimit; 709 } 710 case RelativeCoordinate.WITHIN_LIMITS: 711 return coordinate.limitsBeforeCoordinate.lowerLimit; 712 } 713 714 throw new RuntimeException("Invalid coordinate value."); 715 } 716 717 private boolean areItemsCoveredByBand( 718 @NonNull RelativePoint first, @NonNull RelativePoint second) { 719 720 return doesCoordinateLocationCoverItems(first.mX, second.mX) 721 && doesCoordinateLocationCoverItems(first.mY, second.mY); 722 } 723 724 private boolean doesCoordinateLocationCoverItems( 725 @NonNull RelativeCoordinate pointerCoordinate, 726 @NonNull RelativeCoordinate originCoordinate) { 727 728 if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM 729 && originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) { 730 return false; 731 } 732 733 if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM 734 && originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) { 735 return false; 736 } 737 738 if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS 739 && originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS 740 && pointerCoordinate.limitsBeforeCoordinate.equals( 741 originCoordinate.limitsBeforeCoordinate) 742 && pointerCoordinate.limitsAfterCoordinate.equals( 743 originCoordinate.limitsAfterCoordinate)) { 744 return false; 745 } 746 747 return true; 748 } 749 750 /** 751 * Provides functionality for BandController. Exists primarily to tests that are 752 * fully isolated from RecyclerView. 753 * 754 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types. 755 */ 756 abstract static class GridHost<K> extends BandSelectionHelper.BandHost<K> { 757 758 /** 759 * Remove the listener. 760 * 761 * @param listener 762 */ 763 abstract void removeOnScrollListener(@NonNull OnScrollListener listener); 764 765 /** 766 * @param relativePoint for which to create absolute point. 767 * @return absolute point. 768 */ 769 abstract Point createAbsolutePoint(@NonNull Point relativePoint); 770 771 /** 772 * @param index index of child. 773 * @return rectangle describing child at {@code index}. 774 */ 775 abstract Rect getAbsoluteRectForChildViewAt(int index); 776 777 /** 778 * @param index index of child. 779 * @return child adapter position for the child at {@code index} 780 */ 781 abstract int getAdapterPositionAt(int index); 782 783 /** @return column count. */ 784 abstract int getColumnCount(); 785 786 /** @return number of children visible in the view. */ 787 abstract int getVisibleChildCount(); 788 789 /** 790 * @return true if the item at adapter position is attached to a view. 791 */ 792 abstract boolean hasView(int adapterPosition); 793 } 794 } 795