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