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