diff options
| author | 2015-08-04 12:03:13 -0700 | |
|---|---|---|
| committer | 2015-08-06 11:06:48 -0700 | |
| commit | 155e5615ea21e7345f356487d69a5a8cb336d6cb (patch) | |
| tree | 86709c647db6f7ac57f32e2883130e80096d174d | |
| parent | da7526b4beb97de73b15be5d05a1d1c7a8d591a1 (diff) | |
Add scrolling functionality to BandSelectManager.
Now, if band select reaches the top or bottom of the view, the view scrolls in that direction. Very fast scrolls may cause some items not to be selected properly, but this will be fixed in a future CL.
BUG=20669231
Change-Id: Ie3110156c69ec620713c4929d2583c2ed5ab32df
| -rw-r--r-- | packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java | 168 |
1 files changed, 149 insertions, 19 deletions
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java index f2bde0ec986a..74170f583f7c 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java +++ b/packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.java @@ -18,7 +18,6 @@ package com.android.documentsui; 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 @@ import android.view.View; 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 @@ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { 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 @@ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { 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); } |