+++ /dev/null
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.documentsui;
-
-import static com.android.documentsui.Events.isMouseEvent;
-import static com.android.internal.util.Preconditions.checkState;
-
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.ViewHolder;
-import android.util.Log;
-import android.util.SparseBooleanArray;
-import android.view.MotionEvent;
-import android.view.View;
-
-/**
- * Provides mouse driven band-select support when used in conjuction with {@link RecyclerView} and
- * {@link MultiSelectManager}. This class is responsible for rendering the band select overlay and
- * selecting overlaid items via MultiSelectManager.
- */
-public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener {
-
- private static final int NOT_SELECTED = -1;
- private static final int NOT_SET = -1;
-
- // For debugging purposes.
- private static final String TAG = "BandSelectManager";
- private static final boolean DEBUG = false;
-
- private final RecyclerView mRecyclerView;
- private final MultiSelectManager mSelectManager;
- private final Drawable mRegionSelectorDrawable;
- private final SparseBooleanArray mSelectedByBand = new SparseBooleanArray();
-
- private boolean mIsBandSelectActive = false;
- private Point mOrigin;
- private Point mPointer;
- private Rect mBounds;
-
- // Maintain the last selection made by band, so if bounds shrink back, we can deselect
- // the respective items.
- private int mCursorDeltaY = 0;
- private int mFirstSelected = NOT_SELECTED;
-
- // The time at which the current band selection-induced scroll began. If no scroll is in
- // progress, the value is NOT_SET.
- private long mScrollStartTime = NOT_SET;
- private final Runnable mScrollRunnable = new Runnable() {
- /**
- * The number of milliseconds of scrolling at which scroll speed continues to increase. At
- * first, the scroll starts slowly; then, the rate of scrolling increases until it reaches
- * its maximum value at after this many milliseconds.
- */
- private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
-
- @Override
- public void run() {
- // Compute the number of pixels the pointer's y-coordinate is past the view. Negative
- // values mean the pointer is at or before the top of the view, and positive values mean
- // that the pointer is at or after the bottom of the view. Note that one additional
- // pixel is added here so that the view still scrolls when the pointer is exactly at the
- // top or bottom.
- int pixelsPastView = 0;
- if (mPointer.y <= 0) {
- pixelsPastView = mPointer.y - 1;
- } else if (mPointer.y >= mRecyclerView.getHeight() - 1) {
- pixelsPastView = mPointer.y - mRecyclerView.getHeight() + 1;
- }
-
- if (!mIsBandSelectActive || pixelsPastView == 0) {
- // If band selection is inactive, or if it is active but not at the edge of the
- // view, no scrolling is necessary.
- mScrollStartTime = NOT_SET;
- return;
- }
-
- if (mScrollStartTime == NOT_SET) {
- // If the pointer was previously not at the edge of the view but now is, set the
- // start time for the scroll.
- mScrollStartTime = System.currentTimeMillis();
- }
-
- // Compute the number of pixels to scroll, and scroll that many pixels.
- final int numPixels = computeNumPixelsToScroll(
- pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
- mRecyclerView.scrollBy(0, numPixels);
-
- // Adjust the y-coordinate of the origin the opposite number of pixels so that the
- // origin remains in the same place relative to the view's items.
- mOrigin.y -= numPixels;
- resizeBandSelectRectangle();
-
- mRecyclerView.removeCallbacks(mScrollRunnable);
- mRecyclerView.postOnAnimation(this);
- }
-
- /**
- * Computes the number of pixels to scroll based on how far the pointer is past the end of
- * the view and how long it has been there. Roughly based on ItemTouchHelper's algorithm for
- * computing the number of pixels to scroll when an item is dragged to the end of a
- * {@link RecyclerView}.
- * @param pixelsPastView
- * @param scrollDuration
- * @return
- */
- private int computeNumPixelsToScroll(int pixelsPastView, long scrollDuration) {
- final int maxScrollStep = computeMaxScrollStep(mRecyclerView);
- final int direction = (int) Math.signum(pixelsPastView);
- final int absPastView = Math.abs(pixelsPastView);
-
- // Calculate the ratio of how far out of the view the pointer currently resides to the
- // entire height of the view.
- final float outOfBoundsRatio = Math.min(
- 1.0f, (float) absPastView / mRecyclerView.getHeight());
- // Interpolate this ratio and use it to compute the maximum scroll that should be
- // possible for this step.
- final float cappedScrollStep =
- direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);
-
- // Likewise, calculate the ratio of the time spent in the scroll to the limit.
- final float timeRatio = Math.min(
- 1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
- // Interpolate this ratio and use it to compute the final number of pixels to scroll.
- final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));
-
- // If the final number of pixels to scroll ends up being 0, the view should still scroll
- // at least one pixel.
- return numPixels != 0 ? numPixels : direction;
- }
-
- /**
- * Computes the maximum scroll allowed for a given animation frame. Currently, this
- * defaults to the height of the view, but this could be tweaked if this results in scrolls
- * that are too fast or too slow.
- * @param rv
- * @return
- */
- private int computeMaxScrollStep(RecyclerView rv) {
- return rv.getHeight();
- }
-
- /**
- * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends at
- * (1,1) and quickly approaches 1 near the start of that interval. This ensures that drags
- * that are at the edge or barely past the edge of the view still cause sufficient
- * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if needed.
- * @param ratio A ratio which is in the range [0, 1].
- * @return A "smoothed" value, also in the range [0, 1].
- */
- private float smoothOutOfBoundsRatio(float ratio) {
- return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
- }
-
- /**
- * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1) and
- * stays close to 0 for most input values except those very close to 1. This ensures that
- * scrolls start out very slowly but speed up drastically after the scroll has been in
- * progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used, but this
- * could also be tweaked if needed.
- * @param ratio A ratio which is in the range [0, 1].
- * @return A "smoothed" value, also in the range [0, 1].
- */
- private float smoothTimeRatio(float ratio) {
- return (float) Math.pow(ratio, 5);
- }
- };
-
- /**
- * @param recyclerView
- * @param multiSelectManager
- */
- public BandSelectManager(RecyclerView recyclerView, MultiSelectManager multiSelectManager) {
- mRecyclerView = recyclerView;
- mSelectManager = multiSelectManager;
- mRegionSelectorDrawable =
- mRecyclerView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
-
- mRecyclerView.addOnItemTouchListener(this);
- }
-
- @Override
- public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
- // Only intercept the event if it was triggered by a mouse. If band select is inactive,
- // do not intercept ACTION_UP events as they will not be processed.
- return isMouseEvent(e) &&
- (mIsBandSelectActive || e.getActionMasked() != MotionEvent.ACTION_UP);
- }
-
- @Override
- public void onTouchEvent(RecyclerView rv, MotionEvent e) {
- checkState(isMouseEvent(e));
- processMotionEvent(e);
- }
-
- /**
- * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
- * @param e
- */
- private void processMotionEvent(MotionEvent e) {
- if (mIsBandSelectActive && e.getActionMasked() == MotionEvent.ACTION_UP) {
- endBandSelect();
- return;
- }
-
- mPointer = new Point((int) e.getX(), (int) e.getY());
- if (!mIsBandSelectActive) {
- startBandSelect();
- }
-
- scrollViewIfNecessary();
- resizeBandSelectRectangle();
- selectChildrenCoveredBySelection();
- }
-
- /**
- * Starts band select by adding the drawable to the RecyclerView's overlay.
- */
- private void startBandSelect() {
- if (DEBUG) Log.d(TAG, "Starting band select from (" + mPointer.x + "," + mPointer.y + ").");
- mIsBandSelectActive = true;
- mOrigin = mPointer;
- mRecyclerView.getOverlay().add(mRegionSelectorDrawable);
- }
-
- /**
- * Scrolls the view if necessary.
- */
- private void scrollViewIfNecessary() {
- mRecyclerView.removeCallbacks(mScrollRunnable);
- mScrollRunnable.run();
- mRecyclerView.invalidate();
- }
-
- /**
- * Resizes the band select rectangle by using the origin and the current pointer positoin as
- * two opposite corners of the selection.
- */
- private void resizeBandSelectRectangle() {
- if (mBounds != null) {
- mCursorDeltaY = mPointer.y - mBounds.bottom;
- }
-
- mBounds = new Rect(Math.min(mOrigin.x, mPointer.x),
- Math.min(mOrigin.y, mPointer.y),
- Math.max(mOrigin.x, mPointer.x),
- Math.max(mOrigin.y, mPointer.y));
-
- mRegionSelectorDrawable.setBounds(mBounds);
- }
-
- /**
- * Selects the children covered by the band select overlay by delegating to MultiSelectManager.
- * TODO: Provide a finished implementation. This is down and dirty, proof of concept code.
- * Final optimized implementation, with support for managing offscreen selection to come.
- */
- private void selectChildrenCoveredBySelection() {
-
- // track top and bottom selections. Details on why this is useful below.
- int first = NOT_SELECTED;
- int last = NOT_SELECTED;
-
- for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
-
- View child = mRecyclerView.getChildAt(i);
- ViewHolder holder = mRecyclerView.getChildViewHolder(child);
- Rect childRect = new Rect();
- child.getHitRect(childRect);
-
- boolean shouldSelect = Rect.intersects(childRect, mBounds);
- int position = holder.getAdapterPosition();
-
- // This also allows us to clear the selection of elements
- // that only temporarily entered the bounds of the band.
- if (mSelectedByBand.get(position) && !shouldSelect) {
- mSelectManager.setItemSelected(position, false);
- mSelectedByBand.delete(position);
- }
-
- // We need to keep track of the first and last items selected.
- // We'll use this information along with cursor direction
- // to determine the starting point of the selection.
- // We provide this information to selection manager
- // to enable more natural user interaction when working
- // with Shift+Click and multiple contiguous selection ranges.
- if (shouldSelect) {
- if (first == NOT_SELECTED) {
- first = position;
- } else {
- last = position;
- }
- mSelectManager.setItemSelected(position, true);
- mSelectedByBand.put(position, true);
- }
- }
-
- // Remember which is the last selected item, so we can
- // share that with selection manager when band select ends.
- // It'll use that as it's begin selection point when
- // user SHIFT+Clicks.
- if (mCursorDeltaY < 0 && last != NOT_SELECTED) {
- mFirstSelected = last;
- } else if (mCursorDeltaY > 0 && first != NOT_SELECTED) {
- mFirstSelected = first;
- }
- }
-
- /**
- * Ends band select by removing the overlay.
- */
- private void endBandSelect() {
- if (DEBUG) Log.d(TAG, "Ending band select.");
- mIsBandSelectActive = false;
- mSelectedByBand.clear();
- mRecyclerView.getOverlay().remove(mRegionSelectorDrawable);
- if (mFirstSelected != NOT_SELECTED) {
- mSelectManager.setSelectionFocusBegin(mFirstSelected);
- }
- }
-}
+++ /dev/null
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.documentsui;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import com.google.common.base.Preconditions;
-
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.support.annotation.Nullable;
-import android.support.v7.widget.GridLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.LayoutManager;
-import android.support.v7.widget.RecyclerView.OnScrollListener;
-import android.util.SparseBooleanArray;
-import android.view.View;
-
-/**
- * Provides a band selection item model for views within a RecyclerView. This class queries the
- * RecyclerView to determine where its items are placed; then, once band selection is underway, it
- * alerts listeners of which items are covered by the selections.
- */
-public final class BandSelectMatrix extends RecyclerView.OnScrollListener {
-
- private final RecyclerViewHelper mHelper;
- private final List<OnSelectionChangedListener> mOnSelectionChangedListeners = new ArrayList<>();
-
- // Map from the x-value of the left side of an item to an ordered list of metadata of all items
- // whose x-values are the same. The list is ordered by the y-values of the items in the column.
- // For example, if the first column of the view starts at an x-value of 5, mColumns.get(5) would
- // return a list of all items in that column, with the top-most item first in the list and the
- // bottom-most item last in the list.
- private final Map<Integer, List<ItemData>> mColumns = new HashMap<>();
-
- // List of limits along the x-axis. For example, if the view has two columns, this list will
- // have two elements, each of which lists the lower- and upper-limits of the x-values of the
- // view items. This list is sorted from furthest left to furthest right.
- private final List<Limits> mXLimitsList = new ArrayList<>();
-
- // Like mXLimitsList, but for y-coordinates. Note that this list only contains items which have
- // been in the viewport. Thus, limits which exist in an area of the view to which the view has
- // not scrolled are not present in the list.
- private final List<Limits> mYLimitsList = new ArrayList<>();
-
- // The adapter positions which have been recorded so far.
- private final SparseBooleanArray mRecordedPositions = new SparseBooleanArray();
-
- // Array passed to registered OnSelectionChangedListeners. One array is created and reused
- // throughout the lifetime of the object.
- private final SparseBooleanArray mSelectionForListeners = new SparseBooleanArray();
-
- // The current pointer (in absolute positioning from the top of the view).
- private Point mPointer = null;
-
- // The bounds of the band selection.
- private RelativePoint mRelativeOrigin;
- private RelativePoint mRelativePointer;
-
- BandSelectMatrix(RecyclerViewHelper helper) {
- mHelper = helper;
- mHelper.addOnScrollListener(this);
- }
-
- BandSelectMatrix(RecyclerView rv) {
- this(new RuntimeRecyclerViewHelper(rv));
- }
-
- /**
- * Stops listening to the view's scrolls. Call this function before discarding a
- * BandSelecMatrix object to prevent memory leaks.
- */
- void stopListening() {
- mHelper.removeOnScrollListener(this);
- }
-
- /**
- * Start a band select operation at the given point.
- * @param relativeOrigin The origin of the band select operation, relative to the viewport.
- * For example, if the view is scrolled to the bottom, the top-left of the viewport would
- * have a relative origin of (0, 0), even though its absolute point has a higher y-value.
- */
- void startSelection(Point relativeOrigin) {
- Point absoluteOrigin = mHelper.createAbsolutePoint(relativeOrigin);
- mPointer = new Point(absoluteOrigin.x, absoluteOrigin.y);
-
- processVisibleChildren();
- mRelativeOrigin = new RelativePoint(absoluteOrigin);
- mRelativePointer = new RelativePoint(mPointer);
- computeCurrentSelection();
- notifyListeners();
- }
-
- /**
- * Resizes the selection by adjusting the pointer (i.e., the corner of the selection opposite
- * the origin.
- * @param relativePointer The pointer (opposite of the origin) of the band select operation,
- * relative to the viewport. For example, if the view is scrolled to the bottom, the
- * top-left of the viewport would have a relative origin of (0, 0), even though its absolute
- * point has a higher y-value.
- */
- void resizeSelection(Point relativePointer) {
- mPointer = mHelper.createAbsolutePoint(relativePointer);
- handlePointerMoved();
- }
-
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- if (mPointer == null) {
- return;
- }
-
- mPointer.x += dx;
- mPointer.y += dy;
- processVisibleChildren();
- handlePointerMoved();
- }
-
- /**
- * Queries the view for all children and records their location metadata.
- */
- private void processVisibleChildren() {
- for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
- int adapterPosition = mHelper.getAdapterPositionAt(i);
- if (!mRecordedPositions.get(adapterPosition)) {
- mRecordedPositions.put(adapterPosition, true);
- captureItemLayoutData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
- }
- }
- }
-
- /**
- * Updates the limits lists and column map with the given item metadata.
- * @param absoluteChildRect The absolute rectangle for the child view being processed.
- * @param adapterPosition The position of the child view being processed.
- */
- private void captureItemLayoutData(Rect absoluteChildRect, int adapterPosition) {
- if (mXLimitsList.size() != mHelper.getNumColumns()) {
- // If not all x-limits have been recorded, record this one.
- recordLimits(
- mXLimitsList, new Limits(absoluteChildRect.left, absoluteChildRect.right));
- }
-
- if (mYLimitsList.size() != mHelper.getNumRows()) {
- // If not all y-limits have been recorded, record this one.
- recordLimits(
- mYLimitsList, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
- }
-
- List<ItemData> columnList = mColumns.get(absoluteChildRect.left);
- if (columnList == null) {
- columnList = new ArrayList<ItemData>();
- mColumns.put(absoluteChildRect.left, columnList);
- }
- recordItemData(
- columnList, new ItemData(adapterPosition, absoluteChildRect.top));
- }
-
- /**
- * Ensures limits exists within the sorted list limitsList, and adds it to the list if it does
- * not exist.
- */
- private static void recordLimits(List<Limits> limitsList, Limits limits) {
- int index = Collections.binarySearch(limitsList, limits);
- if (index < 0) {
- limitsList.add(~index, limits);
- }
- }
-
- /**
- * Ensures itemData exists within the sorted list itemDataList, and adds it to the list if it
- * does not exist.
- */
- private static void recordItemData(List<ItemData> itemDataList, ItemData itemData) {
- int index = Collections.binarySearch(itemDataList, itemData);
- if (index < 0) {
- itemDataList.add(~index, itemData);
- }
- }
-
- /**
- * Handles a moved pointer; this function determines whether the pointer movement resulted in a
- * selection change and, if it has, notifies listeners of this change.
- */
- private void handlePointerMoved() {
- RelativePoint old = mRelativePointer;
- mRelativePointer = new RelativePoint(mPointer);
- if (old != null && mRelativePointer.equals(old)) {
- return;
- }
-
- computeCurrentSelection();
- notifyListeners();
- }
-
- /**
- * Computes the currently-selected items.
- */
- private void computeCurrentSelection() {
- Rect selectionRect = mRelativePointer.computeBounds(mRelativeOrigin);
- computePositionsCoveredByRect(selectionRect);
- }
-
- /**
- * Notifies all listeners of a selection change. Note that this function simply passes
- * mSelectionForListeners, so computeCurrentSelection() should be called before this function.
- */
- private void notifyListeners() {
- for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
- listener.onSelectionChanged(mSelectionForListeners);
- }
- }
-
- /**
- * @param rect Rectangle including all covered items.
- */
- private void computePositionsCoveredByRect(@Nullable Rect rect) {
- mSelectionForListeners.clear();
- if (rect == null) {
- // If there is no bounding rectangle, there are no items selected, so just return early.
- return;
- }
-
- int columnIndex = Collections.binarySearch(mXLimitsList, new Limits(rect.left, rect.left));
- Preconditions.checkState(columnIndex >= 0);
-
- for (; columnIndex < mXLimitsList.size() &&
- mXLimitsList.get(columnIndex).lowerLimit <= rect.right; columnIndex++) {
- List<ItemData> positions =
- mColumns.get(mXLimitsList.get(columnIndex).lowerLimit);
- int rowIndex = Collections.binarySearch(positions, new ItemData(0, rect.top));
- if (rowIndex < 0) {
- // If band select occurs after the last item in a row with fewer items than columns,
- // go to the next column. This situation occurs in the last row of the grid when the
- // total number of items is not a multiple of the number of columns (e.g., when 10
- // items exist in a grid with 4 columns).
- continue;
- }
-
- for (; rowIndex < positions.size() &&
- positions.get(rowIndex).offset <= rect.bottom; rowIndex++) {
- mSelectionForListeners.append(positions.get(rowIndex).position, true);
- }
- }
- }
-
- /**
- * Provides functionality for interfacing with the view. In practice, RecyclerViewMatrixHelper
- * should be used; this interface exists solely for the purpose of decoupling the view from
- * this class so that the view can be mocked out for tests.
- */
- interface RecyclerViewHelper {
- public void addOnScrollListener(RecyclerView.OnScrollListener listener);
- public void removeOnScrollListener(RecyclerView.OnScrollListener listener);
- public Point createAbsolutePoint(Point relativePoint);
- public int getVisibleChildCount();
- public int getTotalChildCount();
- public int getNumColumns();
- public int getNumRows();
- public int getAdapterPositionAt(int index);
- public Rect getAbsoluteRectForChildViewAt(int index);
- }
-
- /**
- * Concrete MatrixHelper implementation for use within the Files app.
- */
- static class RuntimeRecyclerViewHelper implements RecyclerViewHelper {
- private final RecyclerView mRecyclerView;
-
- RuntimeRecyclerViewHelper(RecyclerView rv) {
- mRecyclerView = rv;
- }
-
- @Override
- public int getAdapterPositionAt(int index) {
- View child = mRecyclerView.getChildAt(index);
- return mRecyclerView.getChildViewHolder(child).getAdapterPosition();
- }
-
- @Override
- public void addOnScrollListener(OnScrollListener listener) {
- mRecyclerView.addOnScrollListener(listener);
- }
-
- @Override
- public void removeOnScrollListener(OnScrollListener listener) {
- mRecyclerView.removeOnScrollListener(listener);
- }
-
- @Override
- public Point createAbsolutePoint(Point relativePoint) {
- return new Point(relativePoint.x + mRecyclerView.computeHorizontalScrollOffset(),
- relativePoint.y + mRecyclerView.computeVerticalScrollOffset());
- }
-
- @Override
- public Rect getAbsoluteRectForChildViewAt(int index) {
- final View child = mRecyclerView.getChildAt(index);
- final Rect childRect = new Rect();
- child.getHitRect(childRect);
- childRect.left += mRecyclerView.computeHorizontalScrollOffset();
- childRect.right += mRecyclerView.computeHorizontalScrollOffset();
- childRect.top += mRecyclerView.computeVerticalScrollOffset();
- childRect.bottom += mRecyclerView.computeVerticalScrollOffset();
- return childRect;
- }
-
- @Override
- public int getVisibleChildCount() {
- return mRecyclerView.getChildCount();
- }
-
- @Override
- public int getTotalChildCount() {
- return mRecyclerView.getAdapter().getItemCount();
- }
-
- @Override
- public int getNumColumns() {
- LayoutManager layoutManager = mRecyclerView.getLayoutManager();
- if (layoutManager instanceof GridLayoutManager) {
- return ((GridLayoutManager) layoutManager).getSpanCount();
- }
-
- // Otherwise, it is a list with 1 column.
- return 1;
- }
-
- @Override
- public int getNumRows() {
- int numFullColumns = getTotalChildCount() / getNumColumns();
- boolean hasPartiallyFullColumn = getTotalChildCount() % getNumColumns() != 0;
- return numFullColumns + (hasPartiallyFullColumn ? 1 : 0);
- }
- }
-
- /**
- * Listener for changes in which items have been band selected.
- */
- interface OnSelectionChangedListener {
- public void onSelectionChanged(SparseBooleanArray updatedSelection);
- }
-
- void addOnSelectionChangedListener(OnSelectionChangedListener listener) {
- mOnSelectionChangedListeners.add(listener);
- }
-
- void removeOnSelectionChangedListener(OnSelectionChangedListener listener) {
- mOnSelectionChangedListeners.remove(listener);
- }
-
- /**
- * Metadata for an item in the view, consisting of the adapter position and the offset from the
- * top of the view (in pixels). Stored in the mColumns map to model the item grid.
- */
- private static class ItemData implements Comparable<ItemData> {
- int position;
- int offset;
-
- ItemData(int position, int offset) {
- this.position = position;
- this.offset = offset;
- }
-
- @Override
- public int compareTo(ItemData other) {
- // The list of columns is sorted via the offset from the top, so PositionMetadata
- // objects with lower y-values are befor those with higher y-values.
- return offset - other.offset;
- }
- }
-
- /**
- * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
- * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
- * of item columns and the top- and bottom sides of item rows so that it can be determined
- * whether the pointer is located within the bounds of an item.
- */
- private static class Limits implements Comparable<Limits> {
- int lowerLimit;
- int upperLimit;
-
- Limits(int lowerLimit, int upperLimit) {
- this.lowerLimit = lowerLimit;
- this.upperLimit = upperLimit;
- }
-
- @Override
- public int compareTo(Limits other) {
- return lowerLimit - other.lowerLimit;
- }
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof Limits)) {
- return false;
- }
-
- return ((Limits) other).lowerLimit == lowerLimit &&
- ((Limits) other).upperLimit == upperLimit;
- }
- }
-
- /**
- * The location of a coordinate relative to items. This class represents a general area of the
- * view as it relates to band selection rather than an explicit point. For example, two
- * different points within an item are considered to have the same "location" because band
- * selection originating within the item would select the same items no matter which point
- * was used. Same goes for points between items as well as those at the very beginning or end
- * of the view.
- *
- * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
- * advantage of tying the value to the Limits of items along that axis. This allows easy
- * selection of items within those Limits as opposed to a search through every item to see if a
- * given coordinate value falls within those Limits.
- */
- private static class RelativeCoordinate
- implements Comparable<RelativeCoordinate> {
- /**
- * Location describing points after the last known item.
- */
- static final int AFTER_LAST_ITEM = 0;
-
- /**
- * Location describing points before the first known item.
- */
- static final int BEFORE_FIRST_ITEM = 1;
-
- /**
- * Location describing points between two items.
- */
- static final int BETWEEN_TWO_ITEMS = 2;
-
- /**
- * Location describing points within the limits of one item.
- */
- static final int WITHIN_LIMITS = 3;
-
- /**
- * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
- * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
- */
- final int type;
-
- /**
- * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
- * BETWEEN_TWO_ITEMS.
- */
- Limits limitsBeforeCoordinate;
-
- /**
- * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
- */
- Limits limitsAfterCoordinate;
-
- // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
- Limits mFirstKnownItem;
- // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
- Limits mLastKnownItem;
-
- /**
- * @param limitsList The sorted limits list for the coordinate type. If this
- * CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise,
- * mYLimitsList should be pased.
- * @param value The coordinate value.
- */
- RelativeCoordinate(List<Limits> limitsList, int value) {
- Limits dummyLimits = new Limits(value, value);
- int index = Collections.binarySearch(limitsList, dummyLimits);
-
- if (index >= 0) {
- this.type = WITHIN_LIMITS;
- this.limitsBeforeCoordinate = limitsList.get(index);
- } else if (~index == 0) {
- this.type = BEFORE_FIRST_ITEM;
- this.mFirstKnownItem = limitsList.get(0);
- } else if (~index == limitsList.size()) {
- Limits lastLimits = limitsList.get(limitsList.size() - 1);
- if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
- this.type = WITHIN_LIMITS;
- this.limitsBeforeCoordinate = lastLimits;
- } else {
- this.type = AFTER_LAST_ITEM;
- this.mLastKnownItem = lastLimits;
- }
- } else {
- Limits limitsBeforeIndex = limitsList.get(~index - 1);
- if (limitsBeforeIndex.lowerLimit <= value && value <= limitsBeforeIndex.upperLimit) {
- this.type = WITHIN_LIMITS;
- this.limitsBeforeCoordinate = limitsList.get(~index - 1);
- } else {
- this.type = BETWEEN_TWO_ITEMS;
- this.limitsBeforeCoordinate = limitsList.get(~index - 1);
- this.limitsAfterCoordinate = limitsList.get(~index);
- }
- }
- }
-
- int toComparisonValue() {
- if (type == BEFORE_FIRST_ITEM) {
- return mFirstKnownItem.lowerLimit - 1;
- } else if (type == AFTER_LAST_ITEM) {
- return mLastKnownItem.upperLimit + 1;
- } else if (type == BETWEEN_TWO_ITEMS) {
- return limitsBeforeCoordinate.upperLimit + 1;
- } else {
- return limitsBeforeCoordinate.lowerLimit;
- }
- }
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof RelativeCoordinate)) {
- return false;
- }
-
- RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
- return toComparisonValue() == otherCoordinate.toComparisonValue();
- }
-
- @Override
- public int compareTo(RelativeCoordinate other) {
- return toComparisonValue() - other.toComparisonValue();
- }
- }
-
- /**
- * The location of a point relative to the Limits of nearby items; consists of both an x- and
- * y-RelativeCoordinateLocation.
- */
- private class RelativePoint {
- final RelativeCoordinate xLocation;
- final RelativeCoordinate yLocation;
-
- RelativePoint(Point point) {
- this.xLocation = new RelativeCoordinate(mXLimitsList, point.x);
- this.yLocation = new RelativeCoordinate(mYLimitsList, point.y);
- }
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof RelativePoint)) {
- return false;
- }
-
- RelativePoint otherPoint = (RelativePoint) other;
- return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation);
- }
-
- /**
- * Generates a rectangle which contains the items selected by the two points.
- * @param other The other PointLocation. A rectangle will be formed between "this" and
- * "other".
- * @return The rectangle, or null if no items were selected.
- */
- Rect computeBounds(RelativePoint other) {
- if (!areItemsCoveredBySelection(mRelativePointer, mRelativeOrigin)) {
- return null;
- }
-
- RelativeCoordinate minXLocation =
- xLocation.compareTo(other.xLocation) < 0 ? xLocation : other.xLocation;
- RelativeCoordinate maxXLocation =
- minXLocation == xLocation ? other.xLocation : xLocation;
- RelativeCoordinate minYLocation =
- yLocation.compareTo(other.yLocation) < 0 ? yLocation : other.yLocation;
- RelativeCoordinate maxYLocation =
- minYLocation == yLocation ? other.yLocation : yLocation;
-
- Rect rect = new Rect();
- rect.left = getCoordinateValue(minXLocation, mXLimitsList, true);
- rect.right = getCoordinateValue(maxXLocation, mXLimitsList, false);
- rect.top = getCoordinateValue(minYLocation, mYLimitsList, true);
- rect.bottom = getCoordinateValue(maxYLocation, mYLimitsList, false);
- return rect;
- }
-
- int getCoordinateValue(RelativeCoordinate coordinate,
- List<Limits> limitsList, boolean isStartOfRange) {
- switch (coordinate.type) {
- case RelativeCoordinate.BEFORE_FIRST_ITEM:
- return limitsList.get(0).lowerLimit;
- case RelativeCoordinate.AFTER_LAST_ITEM:
- return limitsList.get(limitsList.size() - 1).upperLimit;
- case RelativeCoordinate.BETWEEN_TWO_ITEMS:
- if (isStartOfRange) {
- return coordinate.limitsAfterCoordinate.lowerLimit;
- } else {
- return coordinate.limitsBeforeCoordinate.upperLimit;
- }
- case RelativeCoordinate.WITHIN_LIMITS:
- return coordinate.limitsBeforeCoordinate.lowerLimit;
- }
-
- throw new RuntimeException("Invalid coordinate value.");
- }
- }
-
- private static boolean areItemsCoveredBySelection(
- RelativePoint first, RelativePoint second) {
- return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) &&
- doesCoordinateLocationCoverItems(first.yLocation, second.yLocation);
- }
-
- private static boolean doesCoordinateLocationCoverItems(
- RelativeCoordinate pointerCoordinate,
- RelativeCoordinate originCoordinate) {
- if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM &&
- originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
- return false;
- }
-
- if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM &&
- originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
- return false;
- }
-
- if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
- originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
- pointerCoordinate.limitsBeforeCoordinate.equals(originCoordinate) &&
- pointerCoordinate.limitsAfterCoordinate.equals(originCoordinate)) {
- return false;
- }
-
- return true;
- }
-}
}
@Override
- public void afterActivityCreated(DirectoryFragment fragment) {
- new BandSelectManager(fragment.mRecView, fragment.mSelectionManager);
- }
+ public void afterActivityCreated(DirectoryFragment fragment) {}
}
}
package com.android.documentsui;
+import static com.android.documentsui.Events.isMouseEvent;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkState;
+import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.Adapter;
import android.support.v7.widget.RecyclerView.AdapterDataObserver;
+import android.support.v7.widget.RecyclerView.LayoutManager;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.View;
-
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.support.annotation.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* MultiSelectManager provides support traditional multi-item selection support to RecyclerView.
private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
private Adapter<?> mAdapter;
- private RecyclerViewHelper mHelper;
+ private MultiSelectHelper mHelper;
private boolean mSingleSelect;
+ private BandSelectManager mBandSelectManager;
/**
* @param recyclerView
- * @param gestureDelegate Option delage gesture listener.
+ * @param gestureDelegate Option delegate gesture listener.
* @param mode Selection mode
* @template A gestureDelegate that implements both {@link OnGestureListener}
* and {@link OnDoubleTapListener}
this(
recyclerView.getAdapter(),
- new RecyclerViewHelper() {
- @Override
- public int findEventPosition(MotionEvent e) {
- View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
- return view != null
- ? recyclerView.getChildAdapterPosition(view)
- : RecyclerView.NO_POSITION;
- }
- },
+ new RuntimeRecyclerViewHelper(recyclerView),
mode);
+ mBandSelectManager = new BandSelectManager((RuntimeRecyclerViewHelper) mHelper);
+
GestureDetector.SimpleOnGestureListener listener =
new GestureDetector.SimpleOnGestureListener() {
@Override
CompositeOnGestureListener<? extends Object> compositeListener =
new CompositeOnGestureListener<>(listener, gestureDelegate);
- final GestureDetector detector = new GestureDetector(
- recyclerView.getContext(),
- gestureDelegate == null
- ? listener
- : compositeListener);
+ final GestureDetector detector =
+ new GestureDetector(recyclerView.getContext(), compositeListener);
detector.setOnDoubleTapListener(compositeListener);
new RecyclerView.OnItemTouchListener() {
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
detector.onTouchEvent(e);
- return false;
+
+ // Only intercept the event if it was a mouse-based band selection.
+ return isMouseEvent(e) && (mBandSelectManager.mIsActive ||
+ e.getActionMasked() != MotionEvent.ACTION_UP);
+ }
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ checkState(isMouseEvent(e));
+ mBandSelectManager.processMotionEvent(e);
}
- public void onTouchEvent(RecyclerView rv, MotionEvent e) {}
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
});
}
* @hide
*/
@VisibleForTesting
- MultiSelectManager(Adapter<?> adapter, RecyclerViewHelper helper, int mode) {
+ MultiSelectManager(Adapter<?> adapter, MultiSelectHelper helper, int mode) {
checkNotNull(adapter, "'adapter' cannot be null.");
checkNotNull(helper, "'helper' cannot be null.");
* @param selected
* @return True if the selection state of the item changed.
*/
+ @VisibleForTesting
public boolean setItemSelected(int position, boolean selected) {
if (mSingleSelect && !mSelection.isEmpty()) {
clearSelectionQuietly();
if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
}
- onLongPress(position);
+ onLongPress(position, e.getMetaState());
}
/**
* can be mocked.
*
* @param position
+ * @param metaState as returned from {@link MotionEvent#getMetaState()}.
* @hide
*/
@VisibleForTesting
- void onLongPress(int position) {
+ void onLongPress(int position, int metaState) {
if (position == RecyclerView.NO_POSITION) {
if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
}
- if (toggleSelection(position)) {
- notifySelectionChanged();
- }
+ handlePositionChanged(position, metaState);
}
/**
return false;
}
+ handlePositionChanged(position, metaState);
+ return false;
+ }
+
+ /**
+ * Handles a change caused by a click on the item with the given position. If the Shift key is
+ * held down, this performs a range select; otherwise, it simply toggles the item's selection
+ * state.
+ */
+ private void handlePositionChanged(int position, int metaState) {
if (Events.hasShiftBit(metaState) && mRanger != null) {
mRanger.snapSelection(position);
- } else {
- toggleSelection(position);
- }
- // We're being lazy here notifying even when something might not have changed.
- // To make this more correct, we'd need to update the Ranger class to return
- // information about what has changed.
- notifySelectionChanged();
- return false;
+ // We're being lazy here notifying even when something might not have changed.
+ // To make this more correct, we'd need to update the Ranger class to return
+ // information about what has changed.
+ notifySelectionChanged();
+ } else if (toggleSelection(position)) {
+ notifySelectionChanged();
+ }
}
/**
*/
public static final class Selection {
- private SparseBooleanArray mSelection;
+ // This class tracks selected positions by managing two arrays: the saved selection, and
+ // the total selection. Saved selections are those which have been completed by tapping an
+ // item or by completing a band select operation. Provisional selections are selections
+ // which have been temporarily created by an in-progress band select operation (once the
+ // user releases the mouse button during a band select operation, the selected items
+ // become saved). The total selection is the combination of both the saved selection and
+ // the provisional selection. Tracking both separately is necessary to ensure that saved
+ // selections do not become deselected when they are removed from the provisional selection;
+ // for example, if item A is tapped (and selected), then an in-progress band select covers A
+ // then uncovers A, A should still be selected as it has been saved. To ensure this
+ // behavior, the saved selection must be tracked separately.
+ private SparseBooleanArray mSavedSelection;
+ private SparseBooleanArray mTotalSelection;
public Selection() {
- mSelection = new SparseBooleanArray();
+ mSavedSelection = new SparseBooleanArray();
+ mTotalSelection = new SparseBooleanArray();
}
/**
* @return true if the position is currently selected.
*/
public boolean contains(int position) {
- return mSelection.get(position);
+ return mTotalSelection.get(position);
}
/**
* @return the position value stored at specified index.
*/
public int get(int index) {
- return mSelection.keyAt(index);
+ return mTotalSelection.keyAt(index);
}
/**
* @return size of the selection.
*/
public int size() {
- return mSelection.size();
+ return mTotalSelection.size();
}
/**
* @return true if the selection is empty.
*/
public boolean isEmpty() {
- return mSelection.size() == 0;
+ return mTotalSelection.size() == 0;
+ }
+
+ /**
+ * Sets the provisional selection, which is a temporary selection that can be saved,
+ * canceled, or adjusted at a later time. When a new provision selection is applied, the old
+ * one (if it exists) is abandoned.
+ * @return Array with entry for each position added or removed. Entries which were added
+ * contain a value of true, and entries which were removed contain a value of false.
+ */
+ @VisibleForTesting
+ protected SparseBooleanArray setProvisionalSelection(
+ SparseBooleanArray provisionalSelection) {
+ SparseBooleanArray delta = new SparseBooleanArray();
+
+ for (int i = 0; i < mTotalSelection.size(); i++) {
+ int position = mTotalSelection.keyAt(i);
+ if (!provisionalSelection.get(position) && !mSavedSelection.get(position)) {
+ // Remove each item that used to be in the selection but is unsaved and not in
+ // the new provisional selection.
+ delta.put(position, false);
+ }
+ }
+
+ for (int i = 0; i < provisionalSelection.size(); i++) {
+ int position = provisionalSelection.keyAt(i);
+ if (!mTotalSelection.get(position)) {
+ // Add each item that was not previously in the selection but is in the
+ // new provisional selection.
+ delta.put(position, true);
+ }
+ }
+
+ // Now, iterate through the changes and actually add/remove them to/from
+ // mCurrentSelection. This could not be done in the previous loops because changing the
+ // size of the selection mid-iteration changes iteration order erroneously.
+ for (int i = 0; i < delta.size(); i++) {
+ int position = delta.keyAt(i);
+ if (delta.get(position)) {
+ mTotalSelection.put(position, true);
+ } else {
+ mTotalSelection.delete(position);
+ }
+ }
+
+ return delta;
+ }
+
+ /**
+ * Saves the existing provisional selection. Once the provisional selection is saved,
+ * subsequent provisional selections which are different from this existing one cannot
+ * cause items in this existing provisional selection to become deselected.
+ */
+ @VisibleForTesting
+ protected void applyProvisionalSelection() {
+ mSavedSelection = mTotalSelection.clone();
+ }
+
+ /**
+ * Abandons the existing provisional selection so that all items provisionally selected are
+ * now deselected.
+ */
+ @VisibleForTesting
+ protected void cancelProvisionalSelection() {
+ mTotalSelection = mSavedSelection.clone();
}
private boolean flip(int position) {
/** @hide */
@VisibleForTesting
boolean add(int position) {
- if (!mSelection.get(position)) {
- mSelection.put(position, true);
+ if (!mTotalSelection.get(position)) {
+ mTotalSelection.put(position, true);
+ mSavedSelection.put(position, true);
return true;
}
return false;
/** @hide */
@VisibleForTesting
boolean remove(int position) {
- if (mSelection.get(position)) {
- mSelection.delete(position);
+ if (mTotalSelection.get(position)) {
+ mTotalSelection.delete(position);
+ mSavedSelection.delete(position);
return true;
}
return false;
/**
* Adjusts the selection range to reflect the existence of newly inserted values at
* the specified positions. This has the effect of adjusting all existing selected
- * positions within the specified range accordingly.
+ * positions within the specified range accordingly. Note that this function discards any
+ * provisional selections which may have been applied.
*
* @param startPosition
* @param count
void expand(int startPosition, int count) {
checkState(startPosition >= 0);
checkState(count > 0);
+ cancelProvisionalSelection();
- for (int i = 0; i < mSelection.size(); i++) {
- int itemPosition = mSelection.keyAt(i);
+ for (int i = 0; i < mTotalSelection.size(); i++) {
+ int itemPosition = mTotalSelection.keyAt(i);
if (itemPosition >= startPosition) {
- mSelection.setKeyAt(i, itemPosition + count);
+ mTotalSelection.setKeyAt(i, itemPosition + count);
+ mSavedSelection.setKeyAt(i, itemPosition + count);
}
}
}
/**
* Adjusts the selection range to reflect the removal specified positions. This has
* the effect of adjusting all existing selected positions within the specified range
- * accordingly.
+ * accordingly. Note that this function discards any provisional selections which may have
+ * been applied.
*
* @param startPosition
* @param count The length of the range to collapse. Must be greater than 0.
int endPosition = startPosition + count - 1;
SparseBooleanArray newSelection = new SparseBooleanArray();
- for (int i = 0; i < mSelection.size(); i++) {
- int itemPosition = mSelection.keyAt(i);
+ for (int i = 0; i < mSavedSelection.size(); i++) {
+ int itemPosition = mSavedSelection.keyAt(i);
if (itemPosition < startPosition) {
newSelection.append(itemPosition, true);
} else if (itemPosition > endPosition) {
newSelection.append(itemPosition - count, true);
}
}
- mSelection = newSelection;
+ mSavedSelection = newSelection;
+ cancelProvisionalSelection();
}
/** @hide */
@VisibleForTesting
void clear() {
- mSelection.clear();
+ mSavedSelection.clear();
+ mTotalSelection.clear();
}
/** @hide */
@VisibleForTesting
void copyFrom(Selection source) {
- mSelection = source.mSelection.clone();
+ mSavedSelection = source.mSavedSelection.clone();
+ mTotalSelection = source.mTotalSelection.clone();
}
@Override
return "size=0, items=[]";
}
- StringBuilder buffer = new StringBuilder(mSelection.size() * 28);
+ StringBuilder buffer = new StringBuilder(mTotalSelection.size() * 28);
buffer.append("{size=")
- .append(mSelection.size())
+ .append(mTotalSelection.size())
.append(", ")
.append("items=[");
- for (int i=0; i < mSelection.size(); i++) {
+ for (int i=0; i < mTotalSelection.size(); i++) {
if (i > 0) {
buffer.append(", ");
}
- buffer.append(mSelection.keyAt(i));
+ buffer.append(mTotalSelection.keyAt(i));
}
buffer.append("]}");
return buffer.toString();
@Override
public int hashCode() {
- return mSelection.hashCode();
+ return mSavedSelection.hashCode() ^ mTotalSelection.hashCode();
}
@Override
return false;
}
- return mSelection.equals(((Selection) that).mSelection);
+ return mSavedSelection.equals(((Selection) that).mSavedSelection) &&
+ mTotalSelection.equals(((Selection) that).mTotalSelection);
}
}
- interface RecyclerViewHelper {
+ /**
+ * Provides functionality for MultiSelectManager. In practice, use RuntimeRecyclerViewHelper;
+ * this interface exists only for mocking in tests.
+ */
+ interface MultiSelectHelper {
+ int findEventPosition(MotionEvent e);
+ }
+
+ /**
+ * Provides functionality for BandSelectManager. In practice, use RuntimeRecyclerViewHelper;
+ * this interface exists only for mocking in tests.
+ */
+ interface BandManagerHelper {
+ void drawBand(Rect rect);
int findEventPosition(MotionEvent e);
+ int getHeight();
+ void hideBand();
+ void invalidateView();
+ void postRunnable(Runnable r);
+ void removeCallback(Runnable r);
+ void scrollBy(int dy);
+ }
+
+ /**
+ * Provides functionality for BandSelectModel. In practice, use RuntimeRecyclerViewHelper;
+ * this interface exists only for mocking in tests.
+ */
+ interface BandModelHelper {
+ void addOnScrollListener(RecyclerView.OnScrollListener listener);
+ Point createAbsolutePoint(Point relativePoint);
+ Rect getAbsoluteRectForChildViewAt(int index);
+ int getAdapterPositionAt(int index);
+ int getNumColumns();
+ int getNumRows();
+ int getTotalChildCount();
+ int getVisibleChildCount();
+ void removeOnScrollListener(RecyclerView.OnScrollListener listener);
+ }
+
+ /**
+ * Concrete RecyclerViewHelper implementation for use within the Files app.
+ */
+ private static final class RuntimeRecyclerViewHelper implements MultiSelectHelper,
+ BandManagerHelper, BandModelHelper {
+
+ private final RecyclerView mRecyclerView;
+ private final Drawable mBandSelectRect;
+
+ private boolean mIsOverlayShown = false;
+
+ RuntimeRecyclerViewHelper(RecyclerView rv) {
+ mRecyclerView = rv;
+ mBandSelectRect = mRecyclerView.getContext().getTheme().getDrawable(
+ R.drawable.band_select_overlay);
+ }
+
+ @Override
+ public int getAdapterPositionAt(int index) {
+ View child = mRecyclerView.getChildAt(index);
+ return mRecyclerView.getChildViewHolder(child).getAdapterPosition();
+ }
+
+ @Override
+ public void addOnScrollListener(OnScrollListener listener) {
+ mRecyclerView.addOnScrollListener(listener);
+ }
+
+ @Override
+ public void removeOnScrollListener(OnScrollListener listener) {
+ mRecyclerView.removeOnScrollListener(listener);
+ }
+
+ @Override
+ public Point createAbsolutePoint(Point relativePoint) {
+ return new Point(relativePoint.x + mRecyclerView.computeHorizontalScrollOffset(),
+ relativePoint.y + mRecyclerView.computeVerticalScrollOffset());
+ }
+
+ @Override
+ public Rect getAbsoluteRectForChildViewAt(int index) {
+ final View child = mRecyclerView.getChildAt(index);
+ final Rect childRect = new Rect();
+ child.getHitRect(childRect);
+ childRect.left += mRecyclerView.computeHorizontalScrollOffset();
+ childRect.right += mRecyclerView.computeHorizontalScrollOffset();
+ childRect.top += mRecyclerView.computeVerticalScrollOffset();
+ childRect.bottom += mRecyclerView.computeVerticalScrollOffset();
+ return childRect;
+ }
+
+ @Override
+ public int getVisibleChildCount() {
+ return mRecyclerView.getChildCount();
+ }
+
+ @Override
+ public int getTotalChildCount() {
+ return mRecyclerView.getAdapter().getItemCount();
+ }
+
+ @Override
+ public int getNumColumns() {
+ LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+ if (layoutManager instanceof GridLayoutManager) {
+ return ((GridLayoutManager) layoutManager).getSpanCount();
+ }
+
+ // Otherwise, it is a list with 1 column.
+ return 1;
+ }
+
+ @Override
+ public int getNumRows() {
+ int numFullColumns = getTotalChildCount() / getNumColumns();
+ boolean hasPartiallyFullColumn = getTotalChildCount() % getNumColumns() != 0;
+ return numFullColumns + (hasPartiallyFullColumn ? 1 : 0);
+ }
+
+ @Override
+ public int findEventPosition(MotionEvent e) {
+ View view = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
+ return view != null
+ ? mRecyclerView.getChildAdapterPosition(view)
+ : RecyclerView.NO_POSITION;
+ }
+
+ @Override
+ public int getHeight() {
+ return mRecyclerView.getHeight();
+ }
+
+ @Override
+ public void invalidateView() {
+ mRecyclerView.invalidate();
+ }
+
+ @Override
+ public void postRunnable(Runnable r) {
+ mRecyclerView.postOnAnimation(r);
+ }
+
+ @Override
+ public void removeCallback(Runnable r) {
+ mRecyclerView.removeCallbacks(r);
+ }
+
+ @Override
+ public void scrollBy(int dy) {
+ mRecyclerView.scrollBy(0, dy);
+ }
+
+ @Override
+ public void drawBand(Rect rect) {
+ mBandSelectRect.setBounds(rect);
+
+ if (!mIsOverlayShown) {
+ mRecyclerView.getOverlay().add(mBandSelectRect);
+ }
+ }
+
+ @Override
+ public void hideBand() {
+ mRecyclerView.getOverlay().remove(mBandSelectRect);
+ }
}
public interface Callback {
return false;
}
}
+
+ /**
+ * Provides mouse driven band-select support when used in conjunction with {@link RecyclerView}
+ * and {@link MultiSelectManager}. This class is responsible for rendering the band select
+ * overlay and selecting overlaid items via MultiSelectManager.
+ */
+ public class BandSelectManager implements BandSelectModel.OnSelectionChangedListener {
+
+ private static final int NOT_SET = -1;
+
+ private final BandManagerHelper mHelper;
+
+ private boolean mIsActive;
+ private Point mOrigin;
+ private Point mPointer;
+ private Rect mBounds;
+ private BandSelectModel mModel;
+
+ // The time at which the current band selection-induced scroll began. If no scroll is in
+ // progress, the value is NOT_SET.
+ private long mScrollStartTime = NOT_SET;
+ private final Runnable mViewScroller = new ViewScroller();
+
+ public <T extends BandManagerHelper & BandModelHelper>
+ BandSelectManager(T helper) {
+ mHelper = helper;
+ mModel = new BandSelectModel(helper);
+ mModel.addOnSelectionChangedListener(this);
+ }
+
+ /**
+ * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
+ * @param e
+ */
+ private void processMotionEvent(MotionEvent e) {
+ if (!isMouseEvent(e)) {
+ return;
+ }
+
+ if (mIsActive && e.getActionMasked() == MotionEvent.ACTION_UP) {
+ endBandSelect();
+ return;
+ }
+
+ mPointer = new Point((int) e.getX(), (int) e.getY());
+ if (!mIsActive) {
+ // Only start a band select if the pointer is in margins between items, not
+ // actually within an item's bounds.
+ if (mHelper.findEventPosition(e) != RecyclerView.NO_POSITION) {
+ return;
+ }
+ startBandSelect();
+ } else {
+ mModel.resizeSelection(mPointer);
+ }
+
+ scrollViewIfNecessary();
+ resizeBandSelectRectangle();
+ }
+
+ /**
+ * Starts band select by adding the drawable to the RecyclerView's overlay.
+ */
+ private void startBandSelect() {
+ if (DEBUG) {
+ Log.d(TAG, "Starting band select from (" + mPointer.x + "," + mPointer.y + ").");
+ }
+ mIsActive = true;
+ mOrigin = new Point(mPointer.x, mPointer.y);
+ mModel.startSelection(mOrigin);
+ }
+
+ /**
+ * Scrolls the view if necessary.
+ */
+ private void scrollViewIfNecessary() {
+ mHelper.removeCallback(mViewScroller);
+ mViewScroller.run();
+ mHelper.invalidateView();
+ }
+
+ /**
+ * Resizes the band select rectangle by using the origin and the current pointer position as
+ * two opposite corners of the selection.
+ */
+ private void resizeBandSelectRectangle() {
+ mBounds = new Rect(Math.min(mOrigin.x, mPointer.x),
+ Math.min(mOrigin.y, mPointer.y),
+ Math.max(mOrigin.x, mPointer.x),
+ Math.max(mOrigin.y, mPointer.y));
+ mHelper.drawBand(mBounds);
+ }
+
+ /**
+ * Ends band select by removing the overlay.
+ */
+ private void endBandSelect() {
+ if (DEBUG) Log.d(TAG, "Ending band select.");
+ mIsActive = false;
+ mHelper.hideBand();
+ mSelection.applyProvisionalSelection();
+ mModel.endSelection();
+ int firstSelected = mModel.getPositionNearestOrigin();
+ if (firstSelected != BandSelectModel.NOT_SET) {
+ setSelectionFocusBegin(firstSelected);
+ }
+ }
+
+ @Override
+ public void onSelectionChanged(SparseBooleanArray updatedSelection) {
+ SparseBooleanArray delta = mSelection.setProvisionalSelection(updatedSelection);
+ for (int i = 0; i < delta.size(); i++) {
+ int position = delta.keyAt(i);
+ notifyItemStateChanged(position, delta.get(position));
+ }
+ notifySelectionChanged();
+ }
+
+ private class ViewScroller implements Runnable {
+ /**
+ * The number of milliseconds of scrolling at which scroll speed continues to increase.
+ * At first, the scroll starts slowly; then, the rate of scrolling increases until it
+ * reaches its maximum value at after this many milliseconds.
+ */
+ private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
+
+ @Override
+ public void run() {
+ // Compute the number of pixels the pointer's y-coordinate is past the view.
+ // Negative values mean the pointer is at or before the top of the view, and
+ // positive values mean that the pointer is at or after the bottom of the view. Note
+ // that one additional pixel is added here so that the view still scrolls when the
+ // pointer is exactly at the top or bottom.
+ int pixelsPastView = 0;
+ if (mPointer.y <= 0) {
+ pixelsPastView = mPointer.y - 1;
+ } else if (mPointer.y >= mHelper.getHeight() - 1) {
+ pixelsPastView = mPointer.y - mHelper.getHeight() + 1;
+ }
+
+ if (!mIsActive || pixelsPastView == 0) {
+ // If band selection is inactive, or if it is active but not at the edge of the
+ // view, no scrolling is necessary.
+ mScrollStartTime = NOT_SET;
+ return;
+ }
+
+ if (mScrollStartTime == NOT_SET) {
+ // If the pointer was previously not at the edge of the view but now is, set the
+ // start time for the scroll.
+ mScrollStartTime = System.currentTimeMillis();
+ }
+
+ // Compute the number of pixels to scroll, and scroll that many pixels.
+ final int numPixels = computeScrollDistance(
+ pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
+ mHelper.scrollBy(numPixels);
+
+ // Adjust the y-coordinate of the origin the opposite number of pixels so that the
+ // origin remains in the same place relative to the view's items.
+ mOrigin.y -= numPixels;
+ resizeBandSelectRectangle();
+
+ mHelper.removeCallback(mViewScroller);
+ mHelper.postRunnable(this);
+ }
+
+ /**
+ * Computes the number of pixels to scroll based on how far the pointer is past the end
+ * of the view and how long it has been there. Roughly based on ItemTouchHelper's
+ * algorithm for computing the number of pixels to scroll when an item is dragged to the
+ * end of a {@link RecyclerView}.
+ * @param pixelsPastView
+ * @param scrollDuration
+ * @return
+ */
+ private int computeScrollDistance(int pixelsPastView, long scrollDuration) {
+ final int maxScrollStep = mHelper.getHeight();
+ final int direction = (int) Math.signum(pixelsPastView);
+ final int absPastView = Math.abs(pixelsPastView);
+
+ // Calculate the ratio of how far out of the view the pointer currently resides to
+ // the entire height of the view.
+ final float outOfBoundsRatio = Math.min(
+ 1.0f, (float) absPastView / mHelper.getHeight());
+ // Interpolate this ratio and use it to compute the maximum scroll that should be
+ // possible for this step.
+ final float cappedScrollStep =
+ direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);
+
+ // Likewise, calculate the ratio of the time spent in the scroll to the limit.
+ final float timeRatio = Math.min(
+ 1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
+ // Interpolate this ratio and use it to compute the final number of pixels to
+ // scroll.
+ final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));
+
+ // If the final number of pixels to scroll ends up being 0, the view should still
+ // scroll at least one pixel.
+ return numPixels != 0 ? numPixels : direction;
+ }
+
+ /**
+ * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
+ * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
+ * drags that are at the edge or barely past the edge of the view still cause sufficient
+ * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if
+ * needed.
+ * @param ratio A ratio which is in the range [0, 1].
+ * @return A "smoothed" value, also in the range [0, 1].
+ */
+ private float smoothOutOfBoundsRatio(float ratio) {
+ return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
+ }
+
+ /**
+ * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1)
+ * and stays close to 0 for most input values except those very close to 1. This ensures
+ * that scrolls start out very slowly but speed up drastically after the scroll has been
+ * in progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used,
+ * but this could also be tweaked if needed.
+ * @param ratio A ratio which is in the range [0, 1].
+ * @return A "smoothed" value, also in the range [0, 1].
+ */
+ private float smoothTimeRatio(float ratio) {
+ return (float) Math.pow(ratio, 5);
+ }
+ };
+ }
+
+ /**
+ * Provides a band selection item model for views within a RecyclerView. This class queries the
+ * RecyclerView to determine where its items are placed; then, once band selection is underway,
+ * it alerts listeners of which items are covered by the selections.
+ */
+ public static final class BandSelectModel extends RecyclerView.OnScrollListener {
+
+ public static final int NOT_SET = -1;
+
+ // Enum values used to determine the corner at which the origin is located within the
+ private static final int UPPER = 0x00;
+ private static final int LOWER = 0x01;
+ private static final int LEFT = 0x00;
+ private static final int RIGHT = 0x02;
+ private static final int UPPER_LEFT = UPPER | LEFT;
+ private static final int UPPER_RIGHT = UPPER | RIGHT;
+ private static final int LOWER_LEFT = LOWER | LEFT;
+ private static final int LOWER_RIGHT = LOWER | RIGHT;
+
+ private final BandModelHelper mHelper;
+ private final List<OnSelectionChangedListener> mOnSelectionChangedListeners = new ArrayList<>();
+
+ // Map from the x-value of the left side of an item to an ordered list of metadata of all
+ // items whose x-values are the same. The list is ordered by the y-values of the items in
+ // the column. For example, if the first column of the view starts at an x-value of 5,
+ // mColumns.get(5) would return a list of all items in that column, with the top-most item
+ // first in the list and the bottom-most item last in the list.
+ private final Map<Integer, List<ItemData>> mColumns = new HashMap<>();
+
+ // List of limits along the x-axis. For example, if the view has two columns, this list will
+ // have two elements, each of which lists the lower- and upper-limits of the x-values of the
+ // view items. This list is sorted from furthest left to furthest right.
+ private final List<Limits> mXLimitsList = new ArrayList<>();
+
+ // Like mXLimitsList, but for y-coordinates. Note that this list only contains items which
+ // have been in the viewport. Thus, limits which exist in an area of the view to which the
+ // view has not scrolled are not present in the list.
+ private final List<Limits> mYLimitsList = new ArrayList<>();
+
+ // The adapter positions which have been recorded so far.
+ private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
+
+ // Array passed to registered OnSelectionChangedListeners. One array is created and reused
+ // throughout the lifetime of the object.
+ private final SparseBooleanArray mSelection = new SparseBooleanArray();
+
+ // The current pointer (in absolute positioning from the top of the view).
+ private Point mPointer = null;
+
+ // The bounds of the band selection.
+ private RelativePoint mRelativeOrigin;
+ private RelativePoint mRelativePointer;
+
+ private boolean mIsActive;
+
+ // Tracks where the band select originated from. This is used to determine where selections
+ // should expand from when Shift+click is used.
+ private int mPositionNearestOrigin = NOT_SET;
+
+ BandSelectModel(BandModelHelper helper) {
+ mHelper = helper;
+ mHelper.addOnScrollListener(this);
+ }
+
+ /**
+ * Stops listening to the view's scrolls. Call this function before discarding a
+ * BandSelecModel object to prevent memory leaks.
+ */
+ void stopListening() {
+ mHelper.removeOnScrollListener(this);
+ }
+
+ /**
+ * Start a band select operation at the given point.
+ * @param relativeOrigin The origin of the band select operation, relative to the viewport.
+ * For example, if the view is scrolled to the bottom, the top-left of the viewport
+ * would have a relative origin of (0, 0), even though its absolute point has a higher
+ * y-value.
+ */
+ void startSelection(Point relativeOrigin) {
+ mIsActive = true;
+ mPointer = mHelper.createAbsolutePoint(relativeOrigin);
+
+ recordVisibleChildren();
+ mRelativeOrigin = new RelativePoint(mPointer);
+ mRelativePointer = new RelativePoint(mPointer);
+ computeCurrentSelection();
+ notifyListeners();
+ }
+
+ /**
+ * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
+ * opposite the origin.
+ * @param relativePointer The pointer (opposite of the origin) of the band select operation,
+ * relative to the viewport. For example, if the view is scrolled to the bottom, the
+ * top-left of the viewport would have a relative origin of (0, 0), even though its
+ * absolute point has a higher y-value.
+ */
+ void resizeSelection(Point relativePointer) {
+ mPointer = mHelper.createAbsolutePoint(relativePointer);
+ updateModel();
+ }
+
+ /**
+ * Ends the band selection.
+ */
+ void endSelection() {
+ mIsActive = false;
+ }
+
+ /**
+ * @return The adapter position for the item nearest the origin corresponding to the latest
+ * band select operation, or NOT_SET if the selection did not cover any items.
+ */
+ int getPositionNearestOrigin() {
+ return mPositionNearestOrigin;
+ }
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (!mIsActive) {
+ return;
+ }
+
+ mPointer.x += dx;
+ mPointer.y += dy;
+ recordVisibleChildren();
+ updateModel();
+ }
+
+ /**
+ * Queries the view for all children and records their location metadata.
+ */
+ private void recordVisibleChildren() {
+ for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
+ int adapterPosition = mHelper.getAdapterPositionAt(i);
+ if (!mKnownPositions.get(adapterPosition)) {
+ mKnownPositions.put(adapterPosition, true);
+ recordItemData(
+ mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
+ }
+ }
+ }
+
+ /**
+ * Updates the limits lists and column map with the given item metadata.
+ * @param absoluteChildRect The absolute rectangle for the child view being processed.
+ * @param adapterPosition The position of the child view being processed.
+ */
+ private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
+ if (mXLimitsList.size() != mHelper.getNumColumns()) {
+ // If not all x-limits have been recorded, record this one.
+ recordLimits(
+ mXLimitsList, new Limits(absoluteChildRect.left, absoluteChildRect.right));
+ }
+
+ if (mYLimitsList.size() != mHelper.getNumRows()) {
+ // If not all y-limits have been recorded, record this one.
+ recordLimits(
+ mYLimitsList, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
+ }
+
+ List<ItemData> columnList = mColumns.get(absoluteChildRect.left);
+ if (columnList == null) {
+ columnList = new ArrayList<ItemData>();
+ mColumns.put(absoluteChildRect.left, columnList);
+ }
+ ItemData itemData = new ItemData(adapterPosition, absoluteChildRect.top);
+ int index = Collections.binarySearch(columnList, itemData);
+ if (index < 0) {
+ columnList.add(~index, itemData);
+ }
+ }
+
+ /**
+ * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
+ * does not exist.
+ */
+ private void recordLimits(List<Limits> limitsList, Limits limits) {
+ int index = Collections.binarySearch(limitsList, limits);
+ if (index < 0) {
+ limitsList.add(~index, limits);
+ }
+ }
+
+ /**
+ * Handles a moved pointer; this function determines whether the pointer movement resulted
+ * in a selection change and, if it has, notifies listeners of this change.
+ */
+ private void updateModel() {
+ RelativePoint old = mRelativePointer;
+ mRelativePointer = new RelativePoint(mPointer);
+ if (old != null && mRelativePointer.equals(old)) {
+ return;
+ }
+
+ computeCurrentSelection();
+ notifyListeners();
+ }
+
+ /**
+ * Computes the currently-selected items.
+ */
+ private void computeCurrentSelection() {
+ if (areItemsCoveredBySelection(mRelativePointer, mRelativeOrigin)) {
+ updateSelection(computeBounds());
+ } else {
+ mSelection.clear();
+ mPositionNearestOrigin = NOT_SET;
+ }
+ }
+
+ /**
+ * Notifies all listeners of a selection change. Note that this function simply passes
+ * mSelection, so computeCurrentSelection() should be called before this
+ * function.
+ */
+ private void notifyListeners() {
+ for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
+ listener.onSelectionChanged(mSelection);
+ }
+ }
+
+ /**
+ * @param rect Rectangle including all covered items.
+ */
+ private void updateSelection(Rect rect) {
+ int columnStartIndex =
+ Collections.binarySearch(mXLimitsList, new Limits(rect.left, rect.left));
+ checkState(columnStartIndex >= 0);
+ int columnEndIndex = columnStartIndex;
+
+ for (int i = columnStartIndex;
+ i < mXLimitsList.size() && mXLimitsList.get(i).lowerLimit <= rect.right; i++) {
+ columnEndIndex = i;
+ }
+
+ List<ItemData> firstColumn =
+ mColumns.get(mXLimitsList.get(columnStartIndex).lowerLimit);
+ int rowStartIndex = Collections.binarySearch(firstColumn, new ItemData(0, rect.top));
+ if (rowStartIndex < 0) {
+ mPositionNearestOrigin = NOT_SET;
+ return;
+ }
+
+ int rowEndIndex = rowStartIndex;
+ for (int i = rowStartIndex;
+ i < firstColumn.size() && firstColumn.get(i).offset <= rect.bottom; i++) {
+ rowEndIndex = i;
+ }
+
+ updateSelection(columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex);
+ }
+
+ /**
+ * Computes the selection given the previously-computed start- and end-indices for each
+ * row and column.
+ */
+ private void updateSelection(
+ int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
+ mSelection.clear();
+ for (int column = columnStartIndex; column <= columnEndIndex; column++) {
+ List<ItemData> items = mColumns.get(mXLimitsList.get(column).lowerLimit);
+ for (int row = rowStartIndex; row <= rowEndIndex; row++) {
+ int position = items.get(row).position;
+ mSelection.append(position, true);
+ if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
+ row, rowStartIndex, rowEndIndex)) {
+ // If this is the position nearest the origin, record it now so that it
+ // can be returned by endSelection() later.
+ mPositionNearestOrigin = position;
+ }
+ }
+ }
+ }
+
+ /**
+ * @return Returns true if the position is the nearest to the origin, or, in the case of the
+ * lower-right corner, whether it is possible that the position is the nearest to the
+ * origin. See comment below for reasoning for this special case.
+ */
+ private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
+ int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
+ int corner = computeCornerNearestOrigin();
+ switch (corner) {
+ case UPPER_LEFT:
+ return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
+ case UPPER_RIGHT:
+ return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
+ case LOWER_LEFT:
+ return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
+ case LOWER_RIGHT:
+ // Note that in some cases, the last row will not have as many items as there
+ // are columns (e.g., if there are 4 items and 3 columns, the second row will
+ // only have one item in the first column). This function is invoked for each
+ // position from left to right, so return true for any position in the bottom
+ // row and only the right-most position in the bottom row will be recorded.
+ return rowIndex == rowEndIndex;
+ default:
+ throw new RuntimeException("Invalid corner type.");
+ }
+ }
+
+ /**
+ * Listener for changes in which items have been band selected.
+ */
+ static interface OnSelectionChangedListener {
+ public void onSelectionChanged(SparseBooleanArray updatedSelection);
+ }
+
+ void addOnSelectionChangedListener(OnSelectionChangedListener listener) {
+ mOnSelectionChangedListeners.add(listener);
+ }
+
+ void removeOnSelectionChangedListener(OnSelectionChangedListener listener) {
+ mOnSelectionChangedListeners.remove(listener);
+ }
+
+ /**
+ * Metadata for an item in the view, consisting of the adapter position and the offset from the
+ * top of the view (in pixels). Stored in the mColumns map to model the item grid.
+ */
+ private class ItemData implements Comparable<ItemData> {
+ int position;
+ int offset;
+
+ ItemData(int position, int offset) {
+ this.position = position;
+ this.offset = offset;
+ }
+
+ @Override
+ public int compareTo(ItemData other) {
+ // The list of columns is sorted via the offset from the top, so PositionMetadata
+ // objects with lower y-values are befor those with higher y-values.
+ return offset - other.offset;
+ }
+ }
+
+ /**
+ * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
+ * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
+ * of item columns and the top- and bottom sides of item rows so that it can be determined
+ * whether the pointer is located within the bounds of an item.
+ */
+ private class Limits implements Comparable<Limits> {
+ int lowerLimit;
+ int upperLimit;
+
+ Limits(int lowerLimit, int upperLimit) {
+ this.lowerLimit = lowerLimit;
+ this.upperLimit = upperLimit;
+ }
+
+ @Override
+ public int compareTo(Limits other) {
+ return lowerLimit - other.lowerLimit;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Limits)) {
+ return false;
+ }
+
+ return ((Limits) other).lowerLimit == lowerLimit &&
+ ((Limits) other).upperLimit == upperLimit;
+ }
+ }
+
+ /**
+ * The location of a coordinate relative to items. This class represents a general area of the
+ * view as it relates to band selection rather than an explicit point. For example, two
+ * different points within an item are considered to have the same "location" because band
+ * selection originating within the item would select the same items no matter which point
+ * was used. Same goes for points between items as well as those at the very beginning or end
+ * of the view.
+ *
+ * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
+ * advantage of tying the value to the Limits of items along that axis. This allows easy
+ * selection of items within those Limits as opposed to a search through every item to see if a
+ * given coordinate value falls within those Limits.
+ */
+ private class RelativeCoordinate
+ implements Comparable<RelativeCoordinate> {
+ /**
+ * Location describing points after the last known item.
+ */
+ static final int AFTER_LAST_ITEM = 0;
+
+ /**
+ * Location describing points before the first known item.
+ */
+ static final int BEFORE_FIRST_ITEM = 1;
+
+ /**
+ * Location describing points between two items.
+ */
+ static final int BETWEEN_TWO_ITEMS = 2;
+
+ /**
+ * Location describing points within the limits of one item.
+ */
+ static final int WITHIN_LIMITS = 3;
+
+ /**
+ * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
+ * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
+ */
+ final int type;
+
+ /**
+ * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
+ * BETWEEN_TWO_ITEMS.
+ */
+ Limits limitsBeforeCoordinate;
+
+ /**
+ * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
+ */
+ Limits limitsAfterCoordinate;
+
+ // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
+ Limits mFirstKnownItem;
+ // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
+ Limits mLastKnownItem;
+
+ /**
+ * @param limitsList The sorted limits list for the coordinate type. If this
+ * CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise,
+ * mYLimitsList should be pased.
+ * @param value The coordinate value.
+ */
+ RelativeCoordinate(List<Limits> limitsList, int value) {
+ Limits dummyLimits = new Limits(value, value);
+ int index = Collections.binarySearch(limitsList, dummyLimits);
+
+ if (index >= 0) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = limitsList.get(index);
+ } else if (~index == 0) {
+ this.type = BEFORE_FIRST_ITEM;
+ this.mFirstKnownItem = limitsList.get(0);
+ } else if (~index == limitsList.size()) {
+ Limits lastLimits = limitsList.get(limitsList.size() - 1);
+ if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = lastLimits;
+ } else {
+ this.type = AFTER_LAST_ITEM;
+ this.mLastKnownItem = lastLimits;
+ }
+ } else {
+ Limits limitsBeforeIndex = limitsList.get(~index - 1);
+ if (limitsBeforeIndex.lowerLimit <= value && value <= limitsBeforeIndex.upperLimit) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+ } else {
+ this.type = BETWEEN_TWO_ITEMS;
+ this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+ this.limitsAfterCoordinate = limitsList.get(~index);
+ }
+ }
+ }
+
+ int toComparisonValue() {
+ if (type == BEFORE_FIRST_ITEM) {
+ return mFirstKnownItem.lowerLimit - 1;
+ } else if (type == AFTER_LAST_ITEM) {
+ return mLastKnownItem.upperLimit + 1;
+ } else if (type == BETWEEN_TWO_ITEMS) {
+ return limitsBeforeCoordinate.upperLimit + 1;
+ } else {
+ return limitsBeforeCoordinate.lowerLimit;
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof RelativeCoordinate)) {
+ return false;
+ }
+
+ RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
+ return toComparisonValue() == otherCoordinate.toComparisonValue();
+ }
+
+ @Override
+ public int compareTo(RelativeCoordinate other) {
+ return toComparisonValue() - other.toComparisonValue();
+ }
+ }
+
+ /**
+ * The location of a point relative to the Limits of nearby items; consists of both an x- and
+ * y-RelativeCoordinateLocation.
+ */
+ private class RelativePoint {
+ final RelativeCoordinate xLocation;
+ final RelativeCoordinate yLocation;
+
+ RelativePoint(Point point) {
+ this.xLocation = new RelativeCoordinate(mXLimitsList, point.x);
+ this.yLocation = new RelativeCoordinate(mYLimitsList, point.y);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof RelativePoint)) {
+ return false;
+ }
+
+ RelativePoint otherPoint = (RelativePoint) other;
+ return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation);
+ }
+ }
+
+ /**
+ * Generates a rectangle which contains the items selected by the pointer and origin.
+ * @return The rectangle, or null if no items were selected.
+ */
+ private Rect computeBounds() {
+ Rect rect = new Rect();
+ rect.left = getCoordinateValue(
+ min(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
+ mXLimitsList,
+ true);
+ rect.right = getCoordinateValue(
+ max(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
+ mXLimitsList,
+ false);
+ rect.top = getCoordinateValue(
+ min(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
+ mYLimitsList,
+ true);
+ rect.bottom = getCoordinateValue(
+ max(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
+ mYLimitsList,
+ false);
+ return rect;
+ }
+
+ /**
+ * Computes the corner of the selection nearest the origin.
+ * @return
+ */
+ private int computeCornerNearestOrigin() {
+ int cornerValue = 0;
+
+ if (mRelativeOrigin.yLocation ==
+ min(mRelativeOrigin.yLocation, mRelativePointer.yLocation)) {
+ cornerValue |= UPPER;
+ } else {
+ cornerValue |= LOWER;
+ }
+
+ if (mRelativeOrigin.xLocation ==
+ min(mRelativeOrigin.xLocation, mRelativePointer.xLocation)) {
+ cornerValue |= LEFT;
+ } else {
+ cornerValue |= RIGHT;
+ }
+
+ return cornerValue;
+ }
+
+ private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
+ return first.compareTo(second) < 0 ? first : second;
+ }
+
+ private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) {
+ return first.compareTo(second) > 0 ? first : second;
+ }
+
+ /**
+ * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
+ * coordinate.
+ */
+ private int getCoordinateValue(RelativeCoordinate coordinate,
+ List<Limits> limitsList, boolean isStartOfRange) {
+ switch (coordinate.type) {
+ case RelativeCoordinate.BEFORE_FIRST_ITEM:
+ return limitsList.get(0).lowerLimit;
+ case RelativeCoordinate.AFTER_LAST_ITEM:
+ return limitsList.get(limitsList.size() - 1).upperLimit;
+ case RelativeCoordinate.BETWEEN_TWO_ITEMS:
+ if (isStartOfRange) {
+ return coordinate.limitsAfterCoordinate.lowerLimit;
+ } else {
+ return coordinate.limitsBeforeCoordinate.upperLimit;
+ }
+ case RelativeCoordinate.WITHIN_LIMITS:
+ return coordinate.limitsBeforeCoordinate.lowerLimit;
+ }
+
+ throw new RuntimeException("Invalid coordinate value.");
+ }
+
+ private boolean areItemsCoveredBySelection(
+ RelativePoint first, RelativePoint second) {
+ return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) &&
+ doesCoordinateLocationCoverItems(first.yLocation, second.yLocation);
+ }
+
+ private boolean doesCoordinateLocationCoverItems(
+ RelativeCoordinate pointerCoordinate,
+ RelativeCoordinate originCoordinate) {
+ if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM &&
+ originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
+ return false;
+ }
+
+ if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM &&
+ originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
+ return false;
+ }
+
+ if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
+ originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
+ pointerCoordinate.limitsBeforeCoordinate.equals(
+ originCoordinate.limitsBeforeCoordinate) &&
+ pointerCoordinate.limitsAfterCoordinate.equals(
+ originCoordinate.limitsAfterCoordinate)) {
+ return false;
+ }
+
+ return true;
+ }
+ }
}
import static org.junit.Assert.*;
-import com.android.documentsui.BandSelectMatrix;
+import com.android.documentsui.MultiSelectManager.BandSelectModel;
import android.graphics.Point;
import android.graphics.Rect;
import org.junit.After;
import org.junit.Test;
-public class BandSelectMatrixTest {
+public class BandSelectModelTest {
private static final int VIEW_PADDING_PX = 5;
private static final int CHILD_VIEW_EDGE_PX = 100;
private static final int VIEWPORT_HEIGHT = 500;
- private static BandSelectMatrix matrix;
+ private static BandSelectModel model;
private static TestHelper helper;
private static SparseBooleanArray lastSelection;
private static int viewWidth;
private static void setUp(int numChildren, int numColumns) {
helper = new TestHelper(numChildren, numColumns);
viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX);
- matrix = new BandSelectMatrix(helper);
- matrix.addOnSelectionChangedListener(new BandSelectMatrix.OnSelectionChangedListener() {
+ model = new BandSelectModel(helper);
+ model.addOnSelectionChangedListener(new BandSelectModel.OnSelectionChangedListener() {
@Override
public void onSelectionChanged(SparseBooleanArray updatedSelection) {
@After
public void tearDown() {
- matrix = null;
+ model = null;
helper = null;
lastSelection = null;
}
@Test
public void testSelectionLeftOfItems() {
setUp(20, 5);
- matrix.startSelection(new Point(0, 10));
- matrix.resizeSelection(new Point(1, 11));
+ model.startSelection(new Point(0, 10));
+ model.resizeSelection(new Point(1, 11));
assertSelected(new int[0]);
+ assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
}
@Test
public void testSelectionRightOfItems() {
setUp(20, 4);
- matrix.startSelection(new Point(viewWidth - 1, 10));
- matrix.resizeSelection(new Point(viewWidth - 2, 11));
+ model.startSelection(new Point(viewWidth - 1, 10));
+ model.resizeSelection(new Point(viewWidth - 2, 11));
assertSelected(new int[0]);
+ assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
}
@Test
public void testSelectionAboveItems() {
setUp(20, 4);
- matrix.startSelection(new Point(10, 0));
- matrix.resizeSelection(new Point(11, 1));
+ model.startSelection(new Point(10, 0));
+ model.resizeSelection(new Point(11, 1));
assertSelected(new int[0]);
+ assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
}
@Test
public void testSelectionBelowItems() {
setUp(5, 4);
- matrix.startSelection(new Point(10, VIEWPORT_HEIGHT - 1));
- matrix.resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2));
+ model.startSelection(new Point(10, VIEWPORT_HEIGHT - 1));
+ model.resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2));
assertSelected(new int[0]);
+ assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
}
@Test
public void testVerticalSelectionBetweenItems() {
setUp(20, 4);
- matrix.startSelection(new Point(106, 0));
- matrix.resizeSelection(new Point(107, 200));
+ model.startSelection(new Point(106, 0));
+ model.resizeSelection(new Point(107, 200));
assertSelected(new int[0]);
+ assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
}
@Test
public void testHorizontalSelectionBetweenItems() {
setUp(20, 4);
- matrix.startSelection(new Point(0, 105));
- matrix.resizeSelection(new Point(200, 106));
+ model.startSelection(new Point(0, 105));
+ model.resizeSelection(new Point(200, 106));
assertSelected(new int[0]);
+ assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
}
@Test
public void testGrowingAndShrinkingSelection() {
setUp(20, 4);
- matrix.startSelection(new Point(0, 0));
- matrix.resizeSelection(new Point(5, 5));
+ model.startSelection(new Point(0, 0));
+ model.resizeSelection(new Point(5, 5));
assertSelected(new int[] {0});
- matrix.resizeSelection(new Point(109, 109));
+ model.resizeSelection(new Point(109, 109));
assertSelected(new int[] {0});
- matrix.resizeSelection(new Point(110, 109));
+ model.resizeSelection(new Point(110, 109));
assertSelected(new int[] {0, 1});
- matrix.resizeSelection(new Point(110, 110));
+ model.resizeSelection(new Point(110, 110));
assertSelected(new int[] {0, 1, 4, 5});
- matrix.resizeSelection(new Point(214, 214));
+ model.resizeSelection(new Point(214, 214));
assertSelected(new int[] {0, 1, 4, 5});
- matrix.resizeSelection(new Point(215, 214));
+ model.resizeSelection(new Point(215, 214));
assertSelected(new int[] {0, 1, 2, 4, 5, 6});
- matrix.resizeSelection(new Point(214, 214));
+ model.resizeSelection(new Point(214, 214));
assertSelected(new int[] {0, 1, 4, 5});
- matrix.resizeSelection(new Point(110, 110));
+ model.resizeSelection(new Point(110, 110));
assertSelected(new int[] {0, 1, 4, 5});
- matrix.resizeSelection(new Point(110, 109));
+ model.resizeSelection(new Point(110, 109));
assertSelected(new int[] {0, 1});
- matrix.resizeSelection(new Point(109, 109));
+ model.resizeSelection(new Point(109, 109));
assertSelected(new int[] {0});
- matrix.resizeSelection(new Point(5, 5));
+ model.resizeSelection(new Point(5, 5));
assertSelected(new int[] {0});
- matrix.resizeSelection(new Point(0, 0));
+ model.resizeSelection(new Point(0, 0));
assertSelected(new int[0]);
+ assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
}
@Test
public void testSelectionMovingAroundOrigin() {
setUp(16, 4);
- matrix.startSelection(new Point(210, 210));
- matrix.resizeSelection(new Point(viewWidth - 1, 0));
+ model.startSelection(new Point(210, 210));
+ model.resizeSelection(new Point(viewWidth - 1, 0));
assertSelected(new int[] {2, 3, 6, 7});
- matrix.resizeSelection(new Point(0, 0));
+ model.resizeSelection(new Point(0, 0));
assertSelected(new int[] {0, 1, 4, 5});
- matrix.resizeSelection(new Point(0, 420));
+ model.resizeSelection(new Point(0, 420));
assertSelected(new int[] {8, 9, 12, 13});
- matrix.resizeSelection(new Point(viewWidth - 1, 420));
+ model.resizeSelection(new Point(viewWidth - 1, 420));
assertSelected(new int[] {10, 11, 14, 15});
+ assertEquals(10, model.getPositionNearestOrigin());
}
@Test
public void testScrollingBandSelect() {
setUp(40, 4);
- matrix.startSelection(new Point(0, 0));
- matrix.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
+ model.startSelection(new Point(0, 0));
+ model.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
assertSelected(new int[] {0, 4, 8, 12, 16});
scroll(CHILD_VIEW_EDGE_PX);
assertSelected(new int[] {0, 4, 8, 12, 16, 20});
- matrix.resizeSelection(new Point(200, VIEWPORT_HEIGHT - 1));
+ model.resizeSelection(new Point(200, VIEWPORT_HEIGHT - 1));
assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21});
scroll(CHILD_VIEW_EDGE_PX);
assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21, 24, 25});
scroll(-2 * CHILD_VIEW_EDGE_PX);
assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17});
- matrix.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
+ model.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
assertSelected(new int[] {0, 4, 8, 12, 16});
+ assertEquals(0, model.getPositionNearestOrigin());
}
private static void assertSelected(int[] selectedPositions) {
private static void scroll(int dy) {
assertTrue(helper.verticalOffset + VIEWPORT_HEIGHT + dy <= helper.getTotalHeight());
helper.verticalOffset += dy;
- matrix.onScrolled(null, 0, dy);
+ model.onScrolled(null, 0, dy);
}
- private static final class TestHelper implements BandSelectMatrix.RecyclerViewHelper {
+ private static final class TestHelper implements MultiSelectManager.BandModelHelper {
public int horizontalOffset = 0;
public int verticalOffset = 0;
import static org.junit.Assert.*;
import android.support.v7.widget.RecyclerView;
+import android.util.SparseBooleanArray;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import com.android.documentsui.MultiSelectManager.RecyclerViewHelper;
import com.android.documentsui.MultiSelectManager.Selection;
import org.junit.Before;
@Test
public void mouseClick_NoPosition_ClearsSelection() {
- mManager.onLongPress(7);
+ mManager.onLongPress(7, 0);
click(11);
click(RecyclerView.NO_POSITION);
assertSelection();
@Test
public void longPress_StartsSelectionMode() {
- mManager.onLongPress(7);
+ mManager.onLongPress(7, 0);
assertSelection(7);
}
@Test
public void longPress_SecondPressExtendsSelection() {
- mManager.onLongPress(7);
- mManager.onLongPress(99);
+ mManager.onLongPress(7, 0);
+ mManager.onLongPress(99, 0);
assertSelection(7, 99);
}
@Test
public void singleTapUp_UnselectsSelectedItem() {
- mManager.onLongPress(7);
+ mManager.onLongPress(7, 0);
tap(7);
assertSelection();
}
@Test
public void singleTapUp_NoPosition_ClearsSelection() {
- mManager.onLongPress(7);
+ mManager.onLongPress(7, 0);
tap(11);
tap(RecyclerView.NO_POSITION);
assertSelection();
@Test
public void singleTapUp_ExtendsSelection() {
- mManager.onLongPress(99);
+ mManager.onLongPress(99, 0);
tap(7);
tap(13);
tap(129899);
@Test
public void singleTapUp_ShiftCreatesRangeSelection() {
- mManager.onLongPress(7);
+ mManager.onLongPress(7, 0);
shiftTap(17);
assertRangeSelection(7, 17);
}
@Test
public void singleTapUp_ShiftCreatesRangeSeletion_Backwards() {
- mManager.onLongPress(17);
+ mManager.onLongPress(17, 0);
shiftTap(7);
assertRangeSelection(7, 17);
}
@Test
public void singleTapUp_SecondShiftClickExtendsSelection() {
- mManager.onLongPress(7);
+ mManager.onLongPress(7, 0);
shiftTap(11);
shiftTap(17);
assertRangeSelection(7, 17);
@Test
public void singleTapUp_MultipleContiguousRangesSelected() {
- mManager.onLongPress(7);
+ mManager.onLongPress(7, 0);
shiftTap(11);
tap(20);
shiftTap(25);
@Test
public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick() {
- mManager.onLongPress(7);
+ mManager.onLongPress(7, 0);
shiftTap(17);
shiftTap(10);
assertRangeSelection(7, 10);
@Test
public void singleTapUp_ShiftReducesSelectionRange_FromPreviousShiftClick_Backwards() {
- mManager.onLongPress(17);
+ mManager.onLongPress(17, 0);
shiftTap(7);
shiftTap(14);
assertRangeSelection(14, 17);
@Test
public void singleTapUp_ShiftReversesSelectionDirection() {
- mManager.onLongPress(7);
+ mManager.onLongPress(7, 0);
shiftTap(17);
shiftTap(0);
assertRangeSelection(0, 7);
assertSelection(20);
}
+ @Test
+ public void provisionaSelection() {
+ Selection s = mManager.getSelection();
+ assertSelection();
+
+ SparseBooleanArray provisional = new SparseBooleanArray();
+ provisional.append(1, true);
+ provisional.append(2, true);
+ s.setProvisionalSelection(provisional);
+ assertSelection(1, 2);
+
+ provisional.delete(1);
+ provisional.append(3, true);
+ s.setProvisionalSelection(provisional);
+ assertSelection(2, 3);
+
+ s.applyProvisionalSelection();
+ assertSelection(2, 3);
+
+ provisional.clear();
+ provisional.append(3, true);
+ provisional.append(4, true);
+ s.setProvisionalSelection(provisional);
+ assertSelection(2, 3, 4);
+
+ provisional.delete(3);
+ s.setProvisionalSelection(provisional);
+ assertSelection(2, 3, 4);
+ }
+
private void tap(int position) {
mManager.onSingleTapUp(position, 0, MotionEvent.TOOL_TYPE_MOUSE);
}
assertEquals(selection.toString(), expected, selection.size());
}
- private static final class EventHelper implements RecyclerViewHelper {
+ private static final class EventHelper implements MultiSelectManager.MultiSelectHelper {
+
@Override
public int findEventPosition(MotionEvent e) {
throw new UnsupportedOperationException();
@RunWith(Suite.class)
@SuiteClasses({
- BandSelectMatrixTest.class,
+ BandSelectModelTest.class,
MultiSelectManager_SelectionTest.class,
MultiSelectManagerTest.class
})