Home | History | Annotate | Download | only in selection
      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 
    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      */
    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      */
    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      */
    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
    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      */
    178     int getPositionNearestOrigin() {
    179         return mPositionNearestOrigin;
    180     }
    181 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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 {
    397         abstract void onSelectionChanged(Set<String> updatedSelection);
    398     }
    399 
    400     void addOnSelectionChangedListener(SelectionObserver listener) {
    401         mOnSelectionChangedListeners.add(listener);
    402     }
    403 
    404     /**
    405      * Called when {@link BandSelectionHelper} is finished with a GridModel.
    406      */
    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 
    422         Limits(int lowerLimit, int upperLimit) {
    423             this.lowerLimit = lowerLimit;
    424             this.upperLimit = upperLimit;
    425         }
    426 
    427         @Override
    428         public int compareTo(Limits other) {
    429             return lowerLimit - other.lowerLimit;
    430         }
    431 
    432         @Override
    433         public int hashCode() {
    434             return lowerLimit ^ upperLimit;
    435         }
    436 
    437         @Override
    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
    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          */
    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 
    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
    561         public int hashCode() {
    562             return mFirstKnownItem.lowerLimit
    563                     ^ mLastKnownItem.upperLimit
    564                     ^ limitsBeforeCoordinate.upperLimit
    565                     ^ limitsBeforeCoordinate.lowerLimit;
    566         }
    567 
    568         @Override
    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
    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 
    592         RelativePoint(Point point) {
    593             this.xLocation = new RelativeCoordinate(mColumnBounds, point.x);
    594             this.yLocation = new RelativeCoordinate(mRowBounds, point.y);
    595         }
    596 
    597         @Override
    598         public int hashCode() {
    599             return xLocation.toComparisonValue()
    600                     ^ yLocation.toComparisonValue();
    601         }
    602 
    603         @Override
    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      */
    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      */
    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 
    663     private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
    664         return first.compareTo(second) < 0 ? first : second;
    665     }
    666 
    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      */
    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 
    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 
    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 }