Merge "Add scrolling functionality to BandSelectManager."
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java
index f2bde0e..74170f5 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java
@@ -18,7 +18,6 @@
import static com.android.documentsui.Events.isMouseEvent;
import static com.android.internal.util.Preconditions.checkState;
-import static java.lang.String.format;
import android.graphics.Point;
import android.graphics.Rect;
@@ -38,6 +37,7 @@
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";
@@ -50,14 +50,137 @@
private boolean mIsBandSelectActive = false;
private Point mOrigin;
+ private Point mPointer;
private Rect mBounds;
- // Maintain the last selection made by band, so if bounds shink back, we can unselect
- // the respective items.
- // Track information
+ // 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
@@ -95,41 +218,48 @@
return;
}
- Point point = new Point((int) e.getX(), (int) e.getY());
+ mPointer = new Point((int) e.getX(), (int) e.getY());
if (!mIsBandSelectActive) {
- startBandSelect(point);
+ startBandSelect();
}
- resizeBandSelectRectangle(point);
+ scrollViewIfNecessary();
+ resizeBandSelectRectangle();
selectChildrenCoveredBySelection();
}
/**
* Starts band select by adding the drawable to the RecyclerView's overlay.
- * @param origin The starting point of the selection.
*/
- private void startBandSelect(Point origin) {
- if (DEBUG) Log.d(TAG, "Starting band select from (" + origin.x + "," + origin.y + ").");
+ private void startBandSelect() {
+ if (DEBUG) Log.d(TAG, "Starting band select from (" + mPointer.x + "," + mPointer.y + ").");
mIsBandSelectActive = true;
- mOrigin = origin;
+ 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.
- * @param pointerPosition
*/
- private void resizeBandSelectRectangle(Point pointerPosition) {
-
+ private void resizeBandSelectRectangle() {
if (mBounds != null) {
- mCursorDeltaY = pointerPosition.y - mBounds.bottom;
+ mCursorDeltaY = mPointer.y - mBounds.bottom;
}
- mBounds = new Rect(Math.min(mOrigin.x, pointerPosition.x),
- Math.min(mOrigin.y, pointerPosition.y),
- Math.max(mOrigin.x, pointerPosition.x),
- Math.max(mOrigin.y, pointerPosition.y));
+ 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);
}