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