diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java new file mode 100644 index 0000000000000..f3dc6864fef02 --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java @@ -0,0 +1,1255 @@ +/* + * 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.dirlist; + +import static com.android.documentsui.Shared.DEBUG; +import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY; +import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT; + +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; +import android.view.MotionEvent; +import android.view.View; + +import com.android.documentsui.Events; +import com.android.documentsui.Events.InputEvent; +import com.android.documentsui.Events.MotionInputEvent; +import com.android.documentsui.R; +import com.android.documentsui.dirlist.MultiSelectManager.Selection; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 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 BandController extends RecyclerView.OnScrollListener { + + private static final int NOT_SET = -1; + + private static final String TAG = "BandController"; + + private final Runnable mModelBuilder; + private final SelectionEnvironment mEnvironment; + private final DocumentsAdapter mAdapter; + private final MultiSelectManager mSelectionManager; + private final Runnable mViewScroller = new ViewScroller(); + private final GridModel.OnSelectionChangedListener mGridListener; + + @Nullable private Rect mBounds; + @Nullable private Point mCurrentPosition; + @Nullable private Point mOrigin; + @Nullable private BandController.GridModel 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 Selection mSelection; + + public BandController( + final RecyclerView view, + DocumentsAdapter adapter, + MultiSelectManager selectionManager) { + this(new RuntimeSelectionEnvironment(view), adapter, selectionManager); + + view.addOnItemTouchListener( + new RecyclerView.OnItemTouchListener() { + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { + return handleEvent(new MotionInputEvent(e, view)); + } + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent e) { + if (Events.isMouseEvent(e)) { + processInputEvent(new MotionInputEvent(e, view)); + } + } + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {} + }); + } + + private BandController( + SelectionEnvironment env, + DocumentsAdapter adapter, + MultiSelectManager selectionManager) { + + selectionManager.bindContoller(this); + + mEnvironment = env; + mAdapter = adapter; + mSelectionManager = selectionManager; + + mEnvironment.addOnScrollListener(this); + + mAdapter.registerAdapterDataObserver( + new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + if (isActive()) { + endBandSelect(); + } + } + + @Override + public void onItemRangeChanged( + int startPosition, int itemCount, Object payload) { + // No change in position. Ignoring. + } + + @Override + public void onItemRangeInserted(int startPosition, int itemCount) { + if (isActive()) { + endBandSelect(); + } + } + + @Override + public void onItemRangeRemoved(int startPosition, int itemCount) { + assert(startPosition >= 0); + assert(itemCount > 0); + + // TODO: Should update grid model. + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + throw new UnsupportedOperationException(); + } + }); + + mGridListener = new GridModel.OnSelectionChangedListener() { + + @Override + public void onSelectionChanged(Set updatedSelection) { + BandController.this.onSelectionChanged(updatedSelection); + } + + @Override + public boolean onBeforeItemStateChange(String id, boolean nextState) { + return BandController.this.onBeforeItemStateChange(id, nextState); + } + }; + + mModelBuilder = new Runnable() { + @Override + public void run() { + mModel = new GridModel(mEnvironment, mAdapter); + mModel.addOnSelectionChangedListener(mGridListener); + } + }; + } + + void bindSelection(Selection selection) { + mSelection = selection; + } + + private boolean handleEvent(MotionInputEvent e) { + if (!e.isMouseEvent() && isActive()) { + // Weird things happen if we keep up band select + // when touch events happen. + endBandSelect(); + return false; + } + + // b/23793622 notes the fact that we *never* receive ACTION_DOWN + // events in onTouchEvent. Where it not for this issue, we'd + // push start handling down into handleInputEvent. + if (shouldStart(e)) { + // endBandSelect is handled in handleInputEvent. + startBandSelect(e.getOrigin()); + } else if (isActive() && e.isActionUp()) { + // Same issue here w b/23793622. The ACTION_UP event + // is only evert dispatched to onTouchEvent when + // there is some associated motion. If a user taps + // mouse, but doesn't move, then band select gets + // started BUT not ended. Causing phantom + // bands to appear when the user later clicks to start + // band select. + if (e.isMouseEvent()) { + processInputEvent(e); + } + } + + return isActive(); + } + + private boolean isActive() { + return mModel != null; + } + + /** + * Handle a change in layout by cleaning up and getting rid of the old model and creating + * a new model which will track the new layout. + */ + public void handleLayoutChanged() { + if (mModel != null) { + mModel.removeOnSelectionChangedListener(mGridListener); + mModel.stopListening(); + + // build a new model, all fresh and happy. + mModelBuilder.run(); + } + } + + boolean shouldStart(MotionInputEvent e) { + return !isActive() + && e.isActionDown() // the initial button press + && mAdapter.getItemCount() > 0 + && e.getItemPosition() == RecyclerView.NO_ID; // in empty space + } + + boolean shouldStop(InputEvent input) { + return isActive() + && input.isMouseEvent() + && input.isActionUp(); + } + + /** + * Processes a MotionEvent by starting, ending, or resizing the band select overlay. + * @param input + */ + private void processInputEvent(InputEvent input) { + assert(input.isMouseEvent()); + + if (shouldStop(input)) { + endBandSelect(); + return; + } + + // We shouldn't get any events in this method when band select is not active, + // but it turns some guests show up late to the party. + if (!isActive()) { + return; + } + + mCurrentPosition = input.getOrigin(); + mModel.resizeSelection(input.getOrigin()); + scrollViewIfNecessary(); + resizeBandSelectRectangle(); + } + + /** + * Starts band select by adding the drawable to the RecyclerView's overlay. + */ + private void startBandSelect(Point origin) { + if (DEBUG) Log.d(TAG, "Starting band select @ " + origin); + + mOrigin = origin; + mModelBuilder.run(); // Creates a new selection model. + mModel.startSelection(mOrigin); + } + + /** + * Scrolls the view if necessary. + */ + private void scrollViewIfNecessary() { + mEnvironment.removeCallback(mViewScroller); + mViewScroller.run(); + mEnvironment.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, mCurrentPosition.x), + Math.min(mOrigin.y, mCurrentPosition.y), + Math.max(mOrigin.x, mCurrentPosition.x), + Math.max(mOrigin.y, mCurrentPosition.y)); + mEnvironment.showBand(mBounds); + } + + /** + * Ends band select by removing the overlay. + */ + private void endBandSelect() { + if (DEBUG) Log.d(TAG, "Ending band select."); + + mEnvironment.hideBand(); + mSelection.applyProvisionalSelection(); + mModel.endSelection(); + int firstSelected = mModel.getPositionNearestOrigin(); + if (firstSelected != NOT_SET) { + if (mSelection.contains(mAdapter.getModelId(firstSelected))) { + // TODO: firstSelected should really be lastSelected, we want to anchor the item + // where the mouse-up occurred. + mSelectionManager.setSelectionRangeBegin(firstSelected); + } else { + // TODO: Check if this is really happening. + Log.w(TAG, "First selected by band is NOT in selection!"); + } + } + + mModel = null; + mOrigin = null; + } + + private void onSelectionChanged(Set updatedSelection) { + Map delta = mSelection.setProvisionalSelection(updatedSelection); + for (Map.Entry entry: delta.entrySet()) { + mSelectionManager.notifyItemStateChanged(entry.getKey(), entry.getValue()); + } + mSelectionManager.notifySelectionChanged(); + } + + private boolean onBeforeItemStateChange(String id, boolean nextState) { + return mSelectionManager.notifyBeforeItemStateChange(id, nextState); + } + + 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 (mCurrentPosition.y <= 0) { + pixelsPastView = mCurrentPosition.y - 1; + } else if (mCurrentPosition.y >= mEnvironment.getHeight() - 1) { + pixelsPastView = mCurrentPosition.y - mEnvironment.getHeight() + 1; + } + + if (!isActive() || 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); + mEnvironment.scrollBy(numPixels); + + mEnvironment.removeCallback(mViewScroller); + mEnvironment.runAtNextFrame(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 = mEnvironment.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 / mEnvironment.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); + } + }; + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (!isActive()) { + return; + } + + // 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 -= dy; + resizeBandSelectRectangle(); + } + + /** + * 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. + */ + @VisibleForTesting + static final class GridModel 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 SelectionEnvironment mHelper; + private final DocumentsAdapter mAdapter; + + private final List mOnSelectionChangedListeners = + new ArrayList<>(); + + // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed + // by their y-offset. For example, if the first column of the view starts at an x-value of 5, + // mColumns.get(5) would return an array of positions in that column. Within that array, the + // value for key y is the adapter position for the item whose y-offset is y. + private final SparseArray mColumns = new SparseArray<>(); + + // List of limits along the x-axis (columns). + // This list is sorted from furthest left to furthest right. + private final List mColumnBounds = new ArrayList<>(); + + // List of limits along the y-axis (rows). Note that this list only contains items which + // have been in the viewport. + private final List mRowBounds = 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 Set mSelection = new HashSet<>(); + + // 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; + + GridModel(SelectionEnvironment helper, DocumentsAdapter adapter) { + mHelper = helper; + mAdapter = adapter; + 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) { + recordVisibleChildren(); + if (isEmpty()) { + // The selection band logic works only if there is at least one visible child. + return; + } + + mIsActive = true; + mPointer = mHelper.createAbsolutePoint(relativeOrigin); + 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. + */ + @VisibleForTesting + 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); + // Sometimes the view is not attached, as we notify the multi selection manager + // synchronously, while views are attached asynchronously. As a result items which + // are in the adapter may not actually have a corresponding view (yet). + if (mHelper.hasView(adapterPosition) && + !mHelper.isLayoutItem(adapterPosition) && + !mKnownPositions.get(adapterPosition)) { + mKnownPositions.put(adapterPosition, true); + recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition); + } + } + } + + /** + * Checks if there are any recorded children. + */ + private boolean isEmpty() { + return mColumnBounds.size() == 0 || mRowBounds.size() == 0; + } + + /** + * 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 (mColumnBounds.size() != mHelper.getColumnCount()) { + // If not all x-limits have been recorded, record this one. + recordLimits( + mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right)); + } + + recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom)); + + SparseIntArray columnList = mColumns.get(absoluteChildRect.left); + if (columnList == null) { + columnList = new SparseIntArray(); + mColumns.put(absoluteChildRect.left, columnList); + } + columnList.put(absoluteChildRect.top, adapterPosition); + } + + /** + * Ensures limits exists within the sorted list limitsList, and adds it to the list if it + * does not exist. + */ + private void recordLimits(List limitsList, GridModel.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 (areItemsCoveredByBand(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 (GridModel.OnSelectionChangedListener listener : mOnSelectionChangedListeners) { + listener.onSelectionChanged(mSelection); + } + } + + /** + * @param rect Rectangle including all covered items. + */ + private void updateSelection(Rect rect) { + int columnStart = + Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left)); + assert(columnStart >= 0); + int columnEnd = columnStart; + + for (int i = columnStart; i < mColumnBounds.size() + && mColumnBounds.get(i).lowerLimit <= rect.right; i++) { + columnEnd = i; + } + + int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top)); + if (rowStart < 0) { + mPositionNearestOrigin = NOT_SET; + return; + } + + int rowEnd = rowStart; + for (int i = rowStart; i < mRowBounds.size() + && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) { + rowEnd = i; + } + + updateSelection(columnStart, columnEnd, rowStart, rowEnd); + } + + /** + * 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) { + if (DEBUG) Log.d(TAG, String.format("updateSelection: %d, %d, %d, %d", + columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex)); + + mSelection.clear(); + for (int column = columnStartIndex; column <= columnEndIndex; column++) { + SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit); + for (int row = rowStartIndex; row <= rowEndIndex; row++) { + // The default return value for SparseIntArray.get is 0, which is a valid + // position. Use a sentry value to prevent erroneously selecting item 0. + final int rowKey = mRowBounds.get(row).lowerLimit; + int position = items.get(rowKey, NOT_SET); + if (position != NOT_SET) { + String id = mAdapter.getModelId(position); + if (id != null) { + // The adapter inserts items for UI layout purposes that aren't associated + // with files. Those will have a null model ID. Don't select them. + if (canSelect(id)) { + mSelection.add(id); + } + } + 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 True if the item is selectable. + */ + private boolean canSelect(String id) { + // TODO: Simplify the logic, so the check whether we can select is done in one place. + // Consider injecting FragmentTuner, or move the checks from MultiSelectManager to + // Selection. + for (GridModel.OnSelectionChangedListener listener : mOnSelectionChangedListeners) { + if (!listener.onBeforeItemStateChange(id, true)) { + return false; + } + } + return true; + } + + /** + * @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(Set updatedSelection); + public boolean onBeforeItemStateChange(String id, boolean nextState); + } + + void addOnSelectionChangedListener(GridModel.OnSelectionChangedListener listener) { + mOnSelectionChangedListeners.add(listener); + } + + void removeOnSelectionChangedListener(GridModel.OnSelectionChangedListener listener) { + mOnSelectionChangedListeners.remove(listener); + } + + /** + * 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 { + int lowerLimit; + int upperLimit; + + Limits(int lowerLimit, int upperLimit) { + this.lowerLimit = lowerLimit; + this.upperLimit = upperLimit; + } + + @Override + public int compareTo(GridModel.Limits other) { + return lowerLimit - other.lowerLimit; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof GridModel.Limits)) { + return false; + } + + return ((GridModel.Limits) other).lowerLimit == lowerLimit && + ((GridModel.Limits) other).upperLimit == upperLimit; + } + + @Override + public String toString() { + return "(" + lowerLimit + ", " + 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 { + /** + * 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. + */ + GridModel.Limits limitsBeforeCoordinate; + + /** + * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS. + */ + GridModel.Limits limitsAfterCoordinate; + + // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM. + GridModel.Limits mFirstKnownItem; + // Limits of the last known item; only populated when type == AFTER_LAST_ITEM. + GridModel.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 limitsList, int value) { + int index = Collections.binarySearch(limitsList, new Limits(value, value)); + + 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()) { + GridModel.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 { + GridModel.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 GridModel.RelativeCoordinate)) { + return false; + } + + GridModel.RelativeCoordinate otherCoordinate = (GridModel.RelativeCoordinate) other; + return toComparisonValue() == otherCoordinate.toComparisonValue(); + } + + @Override + public int compareTo(GridModel.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 GridModel.RelativeCoordinate xLocation; + final GridModel.RelativeCoordinate yLocation; + + RelativePoint(Point point) { + this.xLocation = new RelativeCoordinate(mColumnBounds, point.x); + this.yLocation = new RelativeCoordinate(mRowBounds, 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), + mColumnBounds, + true); + rect.right = getCoordinateValue( + max(mRelativeOrigin.xLocation, mRelativePointer.xLocation), + mColumnBounds, + false); + rect.top = getCoordinateValue( + min(mRelativeOrigin.yLocation, mRelativePointer.yLocation), + mRowBounds, + true); + rect.bottom = getCoordinateValue( + max(mRelativeOrigin.yLocation, mRelativePointer.yLocation), + mRowBounds, + 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 GridModel.RelativeCoordinate min(GridModel.RelativeCoordinate first, GridModel.RelativeCoordinate second) { + return first.compareTo(second) < 0 ? first : second; + } + + private GridModel.RelativeCoordinate max(GridModel.RelativeCoordinate first, GridModel.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(GridModel.RelativeCoordinate coordinate, + List 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 areItemsCoveredByBand( + RelativePoint first, RelativePoint second) { + return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) && + doesCoordinateLocationCoverItems(first.yLocation, second.yLocation); + } + + private boolean doesCoordinateLocationCoverItems( + GridModel.RelativeCoordinate pointerCoordinate, + GridModel.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; + } + } + + /** + * Provides functionality for BandController. Exists primarily to tests that are + * fully isolated from RecyclerView. + */ + interface SelectionEnvironment { + void showBand(Rect rect); + void hideBand(); + void addOnScrollListener(RecyclerView.OnScrollListener listener); + void removeOnScrollListener(RecyclerView.OnScrollListener listener); + void scrollBy(int dy); + int getHeight(); + void invalidateView(); + void runAtNextFrame(Runnable r); + void removeCallback(Runnable r); + Point createAbsolutePoint(Point relativePoint); + Rect getAbsoluteRectForChildViewAt(int index); + int getAdapterPositionAt(int index); + int getColumnCount(); + int getChildCount(); + int getVisibleChildCount(); + /** + * Layout items are excluded from the GridModel. + */ + boolean isLayoutItem(int adapterPosition); + /** + * Items may be in the adapter, but without an attached view. + */ + boolean hasView(int adapterPosition); + } + + /** Recycler view facade implementation backed by good ol' RecyclerView. */ + private static final class RuntimeSelectionEnvironment implements SelectionEnvironment { + + private final RecyclerView mView; + private final Drawable mBand; + + private boolean mIsOverlayShown = false; + + RuntimeSelectionEnvironment(RecyclerView view) { + mView = view; + mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay); + } + + @Override + public int getAdapterPositionAt(int index) { + return mView.getChildAdapterPosition(mView.getChildAt(index)); + } + + @Override + public void addOnScrollListener(RecyclerView.OnScrollListener listener) { + mView.addOnScrollListener(listener); + } + + @Override + public void removeOnScrollListener(RecyclerView.OnScrollListener listener) { + mView.removeOnScrollListener(listener); + } + + @Override + public Point createAbsolutePoint(Point relativePoint) { + return new Point(relativePoint.x + mView.computeHorizontalScrollOffset(), + relativePoint.y + mView.computeVerticalScrollOffset()); + } + + @Override + public Rect getAbsoluteRectForChildViewAt(int index) { + final View child = mView.getChildAt(index); + final Rect childRect = new Rect(); + child.getHitRect(childRect); + childRect.left += mView.computeHorizontalScrollOffset(); + childRect.right += mView.computeHorizontalScrollOffset(); + childRect.top += mView.computeVerticalScrollOffset(); + childRect.bottom += mView.computeVerticalScrollOffset(); + return childRect; + } + + @Override + public int getChildCount() { + return mView.getAdapter().getItemCount(); + } + + @Override + public int getVisibleChildCount() { + return mView.getChildCount(); + } + + @Override + public int getColumnCount() { + RecyclerView.LayoutManager layoutManager = mView.getLayoutManager(); + if (layoutManager instanceof GridLayoutManager) { + return ((GridLayoutManager) layoutManager).getSpanCount(); + } + + // Otherwise, it is a list with 1 column. + return 1; + } + + @Override + public int getHeight() { + return mView.getHeight(); + } + + @Override + public void invalidateView() { + mView.invalidate(); + } + + @Override + public void runAtNextFrame(Runnable r) { + mView.postOnAnimation(r); + } + + @Override + public void removeCallback(Runnable r) { + mView.removeCallbacks(r); + } + + @Override + public void scrollBy(int dy) { + mView.scrollBy(0, dy); + } + + @Override + public void showBand(Rect rect) { + mBand.setBounds(rect); + + if (!mIsOverlayShown) { + mView.getOverlay().add(mBand); + } + } + + @Override + public void hideBand() { + mView.getOverlay().remove(mBand); + } + + @Override + public boolean isLayoutItem(int pos) { + // The band selection model only operates on documents and directories. Exclude other + // types of adapter items (e.g. whitespace items like dividers). + RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos); + switch (vh.getItemViewType()) { + case ITEM_TYPE_DOCUMENT: + case ITEM_TYPE_DIRECTORY: + return false; + default: + return true; + } + } + + @Override + public boolean hasView(int pos) { + return mView.findViewHolderForAdapterPosition(pos) != null; + } + } +} \ No newline at end of file diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java index 8cc7c4fb0ecda..1a1fe61fce68d 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -177,6 +177,8 @@ public class DirectoryFragment extends Fragment // Save selection found during creation so it can be restored during directory loading. private Selection mSelection = null; private boolean mSearchMode = false; + + private @Nullable BandController mBandController; private @Nullable ActionMode mActionMode; private DirectoryDragListener mOnDragListener; @@ -299,6 +301,10 @@ public class DirectoryFragment extends Fragment : MultiSelectManager.MODE_SINGLE, null); + if (state.allowMultiple) { + mBandController = new BandController(mRecView, mAdapter, mSelectionManager); + } + mSelectionManager.addCallback(new SelectionModeListener()); mModel = new Model(); @@ -451,7 +457,9 @@ public class DirectoryFragment extends Fragment int pad = getDirectoryPadding(mode); mRecView.setPadding(pad, pad, pad, pad); mRecView.requestLayout(); - mSelectionManager.handleLayoutChanged(); // RecyclerView doesn't do this for us + if (mBandController != null) { + mBandController.handleLayoutChanged(); + } mIconHelper.setViewMode(mode); } diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java index 8852985da5d5f..d570bf197d151 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java @@ -17,35 +17,21 @@ package com.android.documentsui.dirlist; import static com.android.documentsui.Shared.DEBUG; -import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY; -import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT; import android.annotation.IntDef; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; -import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; -import android.util.SparseArray; -import android.util.SparseBooleanArray; -import android.util.SparseIntArray; -import android.view.MotionEvent; -import android.view.View; import com.android.documentsui.Events.InputEvent; -import com.android.documentsui.Events.MotionInputEvent; -import com.android.documentsui.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -72,16 +58,12 @@ public final class MultiSelectManager { private final Selection mSelection = new Selection(); - private final SelectionEnvironment mEnvironment; private final DocumentsAdapter mAdapter; private final List mCallbacks = new ArrayList<>(1); private Range mRanger; private boolean mSingleSelect; - @Nullable private BandController mBandManager; - - /** * @param mode Selection single or multiple selection mode. * @param initialSelection selection state probably preserved in external state. @@ -92,31 +74,7 @@ public final class MultiSelectManager { @SelectionMode int mode, @Nullable Selection initialSelection) { - this(new RuntimeSelectionEnvironment(recyclerView), adapter, mode, initialSelection); - - if (mode == MODE_MULTIPLE) { - // TODO: Don't load this on low memory devices. - mBandManager = new BandController(); - } - - recyclerView.addOnItemTouchListener( - new RecyclerView.OnItemTouchListener() { - @Override - public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - if (mBandManager != null) { - return mBandManager.handleEvent(new MotionInputEvent(e, recyclerView)); - } - return false; - } - - @Override - public void onTouchEvent(RecyclerView rv, MotionEvent e) { - mBandManager.processInputEvent( - new MotionInputEvent(e, recyclerView)); - } - @Override - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {} - }); + this(adapter, mode, initialSelection); } /** @@ -126,15 +84,12 @@ public final class MultiSelectManager { */ @VisibleForTesting MultiSelectManager( - SelectionEnvironment environment, DocumentsAdapter adapter, @SelectionMode int mode, @Nullable Selection initialSelection) { - assert(environment != null); assert(adapter != null); - mEnvironment = environment; mAdapter = adapter; mSingleSelect = mode == MODE_SINGLE; @@ -154,10 +109,6 @@ public final class MultiSelectManager { // Update the selection to remove any disappeared IDs. mSelection.cancelProvisionalSelection(); mSelection.intersect(mModelIds); - - if (mBandManager != null && mBandManager.isActive()) { - mBandManager.endBandSelect(); - } } @Override @@ -188,6 +139,11 @@ public final class MultiSelectManager { }); } + void bindContoller(BandController controller) { + // Provides BandController with access to private mSelection state. + controller.bindSelection(mSelection); + } + /** * Adds {@code callback} such that it will be notified when {@code MultiSelectManager} * events occur. @@ -263,12 +219,6 @@ public final class MultiSelectManager { notifySelectionChanged(); } - public void handleLayoutChanged() { - if (mBandManager != null) { - mBandManager.handleLayoutChanged(); - } - } - /** * Clears the selection, without notifying selection listeners. UI elements still need to be * notified about state changes so that they can update their appearance. @@ -511,7 +461,7 @@ public final class MultiSelectManager { return true; } - private boolean notifyBeforeItemStateChange(String id, boolean nextState) { + boolean notifyBeforeItemStateChange(String id, boolean nextState) { int lastListener = mCallbacks.size() - 1; for (int i = lastListener; i > -1; i--) { if (!mCallbacks.get(i).onBeforeItemStateChange(id, nextState)) { @@ -525,7 +475,7 @@ public final class MultiSelectManager { * Notifies registered listeners when the selection status of a single item * (identified by {@code position}) changes. */ - private void notifyItemStateChanged(String id, boolean selected) { + void notifyItemStateChanged(String id, boolean selected) { assert(id != null); int lastListener = mCallbacks.size() - 1; for (int i = lastListener; i > -1; i--) { @@ -540,7 +490,7 @@ public final class MultiSelectManager { * is complete, e.g. clearingSelection, or updating the single * selection from one item to another. */ - private void notifySelectionChanged() { + void notifySelectionChanged() { int lastListener = mCallbacks.size() - 1; for (int i = lastListener; i > -1; i--) { mCallbacks.get(i).onSelectionChanged(); @@ -920,162 +870,6 @@ public final class MultiSelectManager { }; } - /** - * Provides functionality for BandController. Exists primarily to tests that are - * fully isolated from RecyclerView. - */ - interface SelectionEnvironment { - void showBand(Rect rect); - void hideBand(); - void addOnScrollListener(RecyclerView.OnScrollListener listener); - void removeOnScrollListener(RecyclerView.OnScrollListener listener); - void scrollBy(int dy); - int getHeight(); - void invalidateView(); - void runAtNextFrame(Runnable r); - void removeCallback(Runnable r); - Point createAbsolutePoint(Point relativePoint); - Rect getAbsoluteRectForChildViewAt(int index); - int getAdapterPositionAt(int index); - int getColumnCount(); - int getChildCount(); - int getVisibleChildCount(); - /** - * Layout items are excluded from the GridModel. - */ - boolean isLayoutItem(int adapterPosition); - /** - * Items may be in the adapter, but without an attached view. - */ - boolean hasView(int adapterPosition); - } - - /** Recycler view facade implementation backed by good ol' RecyclerView. */ - private static final class RuntimeSelectionEnvironment implements SelectionEnvironment { - - private final RecyclerView mView; - private final Drawable mBand; - - private boolean mIsOverlayShown = false; - - RuntimeSelectionEnvironment(RecyclerView view) { - mView = view; - mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay); - } - - @Override - public int getAdapterPositionAt(int index) { - return mView.getChildAdapterPosition(mView.getChildAt(index)); - } - - @Override - public void addOnScrollListener(RecyclerView.OnScrollListener listener) { - mView.addOnScrollListener(listener); - } - - @Override - public void removeOnScrollListener(RecyclerView.OnScrollListener listener) { - mView.removeOnScrollListener(listener); - } - - @Override - public Point createAbsolutePoint(Point relativePoint) { - return new Point(relativePoint.x + mView.computeHorizontalScrollOffset(), - relativePoint.y + mView.computeVerticalScrollOffset()); - } - - @Override - public Rect getAbsoluteRectForChildViewAt(int index) { - final View child = mView.getChildAt(index); - final Rect childRect = new Rect(); - child.getHitRect(childRect); - childRect.left += mView.computeHorizontalScrollOffset(); - childRect.right += mView.computeHorizontalScrollOffset(); - childRect.top += mView.computeVerticalScrollOffset(); - childRect.bottom += mView.computeVerticalScrollOffset(); - return childRect; - } - - @Override - public int getChildCount() { - return mView.getAdapter().getItemCount(); - } - - @Override - public int getVisibleChildCount() { - return mView.getChildCount(); - } - - @Override - public int getColumnCount() { - RecyclerView.LayoutManager layoutManager = mView.getLayoutManager(); - if (layoutManager instanceof GridLayoutManager) { - return ((GridLayoutManager) layoutManager).getSpanCount(); - } - - // Otherwise, it is a list with 1 column. - return 1; - } - - @Override - public int getHeight() { - return mView.getHeight(); - } - - @Override - public void invalidateView() { - mView.invalidate(); - } - - @Override - public void runAtNextFrame(Runnable r) { - mView.postOnAnimation(r); - } - - @Override - public void removeCallback(Runnable r) { - mView.removeCallbacks(r); - } - - @Override - public void scrollBy(int dy) { - mView.scrollBy(0, dy); - } - - @Override - public void showBand(Rect rect) { - mBand.setBounds(rect); - - if (!mIsOverlayShown) { - mView.getOverlay().add(mBand); - } - } - - @Override - public void hideBand() { - mView.getOverlay().remove(mBand); - } - - @Override - public boolean isLayoutItem(int pos) { - // The band selection model only operates on documents and directories. Exclude other - // types of adapter items (e.g. whitespace items like dividers). - RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos); - switch (vh.getItemViewType()) { - case ITEM_TYPE_DOCUMENT: - case ITEM_TYPE_DIRECTORY: - return false; - default: - return true; - } - } - - @Override - public boolean hasView(int pos) { - return mView.findViewHolderForAdapterPosition(pos) != null; - } - } - public interface Callback { /** * Called when an item is selected or unselected while in selection mode. @@ -1101,958 +895,4 @@ public final class MultiSelectManager { */ public void onSelectionChanged(); } - - /** - * 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 BandController extends RecyclerView.OnScrollListener - implements GridModel.OnSelectionChangedListener { - - private static final int NOT_SET = -1; - - private final Runnable mModelBuilder; - - @Nullable private Rect mBounds; - @Nullable private Point mCurrentPosition; - @Nullable private Point mOrigin; - @Nullable private GridModel 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 BandController() { - mEnvironment.addOnScrollListener(this); - - mModelBuilder = new Runnable() { - @Override - public void run() { - mModel = new GridModel(mEnvironment, mAdapter); - mModel.addOnSelectionChangedListener(BandController.this); - } - }; - } - - public boolean handleEvent(MotionInputEvent e) { - // b/23793622 notes the fact that we *never* receive ACTION_DOWN - // events in onTouchEvent. Where it not for this issue, we'd - // push start handling down into handleInputEvent. - if (mBandManager.shouldStart(e)) { - // endBandSelect is handled in handleInputEvent. - mBandManager.startBandSelect(e.getOrigin()); - } else if (mBandManager.isActive() - && e.isMouseEvent() - && e.isActionUp()) { - // Same issue here w b/23793622. The ACTION_UP event - // is only evert dispatched to onTouchEvent when - // there is some associated motion. If a user taps - // mouse, but doesn't move, then band select gets - // started BUT not ended. Causing phantom - // bands to appear when the user later clicks to start - // band select. - mBandManager.processInputEvent(e); - } - - return isActive(); - } - - private boolean isActive() { - return mModel != null; - } - - /** - * Handle a change in layout by cleaning up and getting rid of the old model and creating - * a new model which will track the new layout. - */ - public void handleLayoutChanged() { - if (mModel != null) { - mModel.removeOnSelectionChangedListener(this); - mModel.stopListening(); - - // build a new model, all fresh and happy. - mModelBuilder.run(); - } - } - - boolean shouldStart(MotionInputEvent e) { - return !isActive() - && e.isMouseEvent() // a mouse - && e.isActionDown() // the initial button press - && mAdapter.getItemCount() > 0 - && e.getItemPosition() == RecyclerView.NO_ID; // in empty space - } - - boolean shouldStop(InputEvent input) { - return isActive() - && input.isMouseEvent() - && input.isActionUp(); - } - - /** - * Processes a MotionEvent by starting, ending, or resizing the band select overlay. - * @param input - */ - private void processInputEvent(InputEvent input) { - assert(input.isMouseEvent()); - - if (shouldStop(input)) { - endBandSelect(); - return; - } - - // We shouldn't get any events in this method when band select is not active, - // but it turns some guests show up late to the party. - if (!isActive()) { - return; - } - - mCurrentPosition = input.getOrigin(); - mModel.resizeSelection(input.getOrigin()); - scrollViewIfNecessary(); - resizeBandSelectRectangle(); - } - - /** - * Starts band select by adding the drawable to the RecyclerView's overlay. - */ - private void startBandSelect(Point origin) { - if (DEBUG) Log.d(TAG, "Starting band select @ " + origin); - - mOrigin = origin; - mModelBuilder.run(); // Creates a new selection model. - mModel.startSelection(mOrigin); - } - - /** - * Scrolls the view if necessary. - */ - private void scrollViewIfNecessary() { - mEnvironment.removeCallback(mViewScroller); - mViewScroller.run(); - mEnvironment.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, mCurrentPosition.x), - Math.min(mOrigin.y, mCurrentPosition.y), - Math.max(mOrigin.x, mCurrentPosition.x), - Math.max(mOrigin.y, mCurrentPosition.y)); - mEnvironment.showBand(mBounds); - } - - /** - * Ends band select by removing the overlay. - */ - private void endBandSelect() { - if (DEBUG) Log.d(TAG, "Ending band select."); - - mEnvironment.hideBand(); - mSelection.applyProvisionalSelection(); - mModel.endSelection(); - int firstSelected = mModel.getPositionNearestOrigin(); - if (firstSelected != NOT_SET) { - if (mSelection.contains(mAdapter.getModelId(firstSelected))) { - // TODO: firstSelected should really be lastSelected, we want to anchor the item - // where the mouse-up occurred. - setSelectionRangeBegin(firstSelected); - } else { - // TODO: Check if this is really happening. - Log.w(TAG, "First selected by band is NOT in selection!"); - } - } - - mModel = null; - mOrigin = null; - } - - @Override - public void onSelectionChanged(Set updatedSelection) { - Map delta = mSelection.setProvisionalSelection(updatedSelection); - for (Map.Entry entry: delta.entrySet()) { - notifyItemStateChanged(entry.getKey(), entry.getValue()); - } - notifySelectionChanged(); - } - - @Override - public boolean onBeforeItemStateChange(String id, boolean nextState) { - return notifyBeforeItemStateChange(id, nextState); - } - - 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 (mCurrentPosition.y <= 0) { - pixelsPastView = mCurrentPosition.y - 1; - } else if (mCurrentPosition.y >= mEnvironment.getHeight() - 1) { - pixelsPastView = mCurrentPosition.y - mEnvironment.getHeight() + 1; - } - - if (!isActive() || 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); - mEnvironment.scrollBy(numPixels); - - mEnvironment.removeCallback(mViewScroller); - mEnvironment.runAtNextFrame(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 = mEnvironment.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 / mEnvironment.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); - } - }; - - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - if (!isActive()) { - return; - } - - // 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 -= dy; - resizeBandSelectRectangle(); - } - } - - /** - * 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 GridModel 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 SelectionEnvironment mHelper; - private final DocumentsAdapter mAdapter; - - private final List mOnSelectionChangedListeners = - new ArrayList<>(); - - // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed - // by their y-offset. For example, if the first column of the view starts at an x-value of 5, - // mColumns.get(5) would return an array of positions in that column. Within that array, the - // value for key y is the adapter position for the item whose y-offset is y. - private final SparseArray mColumns = new SparseArray<>(); - - // List of limits along the x-axis (columns). - // This list is sorted from furthest left to furthest right. - private final List mColumnBounds = new ArrayList<>(); - - // List of limits along the y-axis (rows). Note that this list only contains items which - // have been in the viewport. - private final List mRowBounds = 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 Set mSelection = new HashSet<>(); - - // 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; - - GridModel(SelectionEnvironment helper, DocumentsAdapter adapter) { - mHelper = helper; - mAdapter = adapter; - 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) { - recordVisibleChildren(); - if (isEmpty()) { - // The selection band logic works only if there is at least one visible child. - return; - } - - mIsActive = true; - mPointer = mHelper.createAbsolutePoint(relativeOrigin); - 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. - */ - @VisibleForTesting - 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); - // Sometimes the view is not attached, as we notify the multi selection manager - // synchronously, while views are attached asynchronously. As a result items which - // are in the adapter may not actually have a corresponding view (yet). - if (mHelper.hasView(adapterPosition) && - !mHelper.isLayoutItem(adapterPosition) && - !mKnownPositions.get(adapterPosition)) { - mKnownPositions.put(adapterPosition, true); - recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition); - } - } - } - - /** - * Checks if there are any recorded children. - */ - private boolean isEmpty() { - return mColumnBounds.size() == 0 || mRowBounds.size() == 0; - } - - /** - * 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 (mColumnBounds.size() != mHelper.getColumnCount()) { - // If not all x-limits have been recorded, record this one. - recordLimits( - mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right)); - } - - recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom)); - - SparseIntArray columnList = mColumns.get(absoluteChildRect.left); - if (columnList == null) { - columnList = new SparseIntArray(); - mColumns.put(absoluteChildRect.left, columnList); - } - columnList.put(absoluteChildRect.top, adapterPosition); - } - - /** - * Ensures limits exists within the sorted list limitsList, and adds it to the list if it - * does not exist. - */ - private void recordLimits(List 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 (areItemsCoveredByBand(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 columnStart = - Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left)); - assert(columnStart >= 0); - int columnEnd = columnStart; - - for (int i = columnStart; i < mColumnBounds.size() - && mColumnBounds.get(i).lowerLimit <= rect.right; i++) { - columnEnd = i; - } - - int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top)); - if (rowStart < 0) { - mPositionNearestOrigin = NOT_SET; - return; - } - - int rowEnd = rowStart; - for (int i = rowStart; i < mRowBounds.size() - && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) { - rowEnd = i; - } - - updateSelection(columnStart, columnEnd, rowStart, rowEnd); - } - - /** - * 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) { - if (DEBUG) Log.d(TAG, String.format("updateSelection: %d, %d, %d, %d", - columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex)); - - mSelection.clear(); - for (int column = columnStartIndex; column <= columnEndIndex; column++) { - SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit); - for (int row = rowStartIndex; row <= rowEndIndex; row++) { - // The default return value for SparseIntArray.get is 0, which is a valid - // position. Use a sentry value to prevent erroneously selecting item 0. - final int rowKey = mRowBounds.get(row).lowerLimit; - int position = items.get(rowKey, NOT_SET); - if (position != NOT_SET) { - String id = mAdapter.getModelId(position); - if (id != null) { - // The adapter inserts items for UI layout purposes that aren't associated - // with files. Those will have a null model ID. Don't select them. - if (canSelect(id)) { - mSelection.add(id); - } - } - 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 True if the item is selectable. - */ - private boolean canSelect(String id) { - // TODO: Simplify the logic, so the check whether we can select is done in one place. - // Consider injecting FragmentTuner, or move the checks from MultiSelectManager to - // Selection. - for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) { - if (!listener.onBeforeItemStateChange(id, true)) { - return false; - } - } - return true; - } - - /** - * @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(Set updatedSelection); - public boolean onBeforeItemStateChange(String id, boolean nextState); - } - - void addOnSelectionChangedListener(OnSelectionChangedListener listener) { - mOnSelectionChangedListeners.add(listener); - } - - void removeOnSelectionChangedListener(OnSelectionChangedListener listener) { - mOnSelectionChangedListeners.remove(listener); - } - - /** - * 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 { - 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; - } - - @Override - public String toString() { - return "(" + lowerLimit + ", " + 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 { - /** - * 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 limitsList, int value) { - int index = Collections.binarySearch(limitsList, new Limits(value, value)); - - 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(mColumnBounds, point.x); - this.yLocation = new RelativeCoordinate(mRowBounds, 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), - mColumnBounds, - true); - rect.right = getCoordinateValue( - max(mRelativeOrigin.xLocation, mRelativePointer.xLocation), - mColumnBounds, - false); - rect.top = getCoordinateValue( - min(mRelativeOrigin.yLocation, mRelativePointer.yLocation), - mRowBounds, - true); - rect.bottom = getCoordinateValue( - max(mRelativeOrigin.yLocation, mRelativePointer.yLocation), - mRowBounds, - 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 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 areItemsCoveredByBand( - 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; - } - } } diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/BandController_GridModelTest.java similarity index 97% rename from packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java rename to packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/BandController_GridModelTest.java index e401de1607080..59547adc08e05 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/BandController_GridModelTest.java @@ -16,7 +16,7 @@ package com.android.documentsui.dirlist; -import static com.android.documentsui.dirlist.MultiSelectManager.GridModel.NOT_SET; +import static com.android.documentsui.dirlist.BandController.GridModel.NOT_SET; import android.graphics.Point; import android.graphics.Rect; @@ -24,14 +24,14 @@ import android.support.v7.widget.RecyclerView.OnScrollListener; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; -import com.android.documentsui.dirlist.MultiSelectManager.GridModel; +import com.android.documentsui.dirlist.BandController.GridModel; import java.util.ArrayList; import java.util.List; import java.util.Set; @SmallTest -public class MultiSelectManager_GridModelTest extends AndroidTestCase { +public class BandController_GridModelTest extends AndroidTestCase { private static final int VIEW_PADDING_PX = 5; private static final int CHILD_VIEW_EDGE_PX = 100; @@ -279,7 +279,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase { model.onScrolled(null, 0, dy); } - private static final class TestEnvironment implements MultiSelectManager.SelectionEnvironment { + private static final class TestEnvironment implements BandController.SelectionEnvironment { private final int mNumColumns; private final int mNumRows; diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java index 9447d9c18a835..9401da8fb5daf 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java @@ -23,6 +23,7 @@ import android.util.SparseBooleanArray; import com.android.documentsui.TestInputEvent; import com.android.documentsui.dirlist.MultiSelectManager.Selection; + import com.google.common.collect.Lists; import java.util.ArrayList; @@ -43,14 +44,12 @@ public class MultiSelectManagerTest extends AndroidTestCase { private MultiSelectManager mManager; private TestCallback mCallback; - private TestSelectionEnvironment mEnv; private TestDocumentsAdapter mAdapter; public void setUp() throws Exception { mCallback = new TestCallback(); - mEnv = new TestSelectionEnvironment(items); mAdapter = new TestDocumentsAdapter(items); - mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_MULTIPLE, null); + mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE, null); mManager.addCallback(mCallback); } @@ -174,7 +173,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { } public void testSingleSelectMode() { - mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null); + mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE, null); mManager.addCallback(mCallback); longPress(20); tap(13); @@ -182,7 +181,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { } public void testSingleSelectMode_ShiftTap() { - mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null); + mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE, null); mManager.addCallback(mCallback); longPress(13); shiftTap(20); @@ -229,7 +228,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { } public void testRangeSelection_singleSelect() { - mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null); + mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE, null); mManager.addCallback(mCallback); mManager.startRangeSelection(11); mManager.snapRangeSelection(19); diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java index f56476978a4eb..b69787c697cea 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java @@ -20,7 +20,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.support.v7.widget.RecyclerView.OnScrollListener; -import com.android.documentsui.dirlist.MultiSelectManager.SelectionEnvironment; +import com.android.documentsui.dirlist.BandController.SelectionEnvironment; import java.util.List;