Loading packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.javadeleted 100644 → 0 +0 −335 Original line number Diff line number Diff line /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui; import static com.android.documentsui.Events.isMouseEvent; import static com.android.internal.util.Preconditions.checkState; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.ViewHolder; import android.util.Log; import android.util.SparseBooleanArray; import android.view.MotionEvent; import android.view.View; /** * Provides mouse driven band-select support when used in conjuction with {@link RecyclerView} and * {@link MultiSelectManager}. This class is responsible for rendering the band select overlay and * selecting overlaid items via MultiSelectManager. */ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { private static final int NOT_SELECTED = -1; private static final int NOT_SET = -1; // For debugging purposes. private static final String TAG = "BandSelectManager"; private static final boolean DEBUG = false; private final RecyclerView mRecyclerView; private final MultiSelectManager mSelectManager; private final Drawable mRegionSelectorDrawable; private final SparseBooleanArray mSelectedByBand = new SparseBooleanArray(); private boolean mIsBandSelectActive = false; private Point mOrigin; private Point mPointer; private Rect mBounds; // Maintain the last selection made by band, so if bounds shrink back, we can deselect // the respective items. private int mCursorDeltaY = 0; private int mFirstSelected = NOT_SELECTED; // The time at which the current band selection-induced scroll began. If no scroll is in // progress, the value is NOT_SET. private long mScrollStartTime = NOT_SET; private final Runnable mScrollRunnable = new Runnable() { /** * The number of milliseconds of scrolling at which scroll speed continues to increase. At * first, the scroll starts slowly; then, the rate of scrolling increases until it reaches * its maximum value at after this many milliseconds. */ private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; @Override public void run() { // Compute the number of pixels the pointer's y-coordinate is past the view. Negative // values mean the pointer is at or before the top of the view, and positive values mean // that the pointer is at or after the bottom of the view. Note that one additional // pixel is added here so that the view still scrolls when the pointer is exactly at the // top or bottom. int pixelsPastView = 0; if (mPointer.y <= 0) { pixelsPastView = mPointer.y - 1; } else if (mPointer.y >= mRecyclerView.getHeight() - 1) { pixelsPastView = mPointer.y - mRecyclerView.getHeight() + 1; } if (!mIsBandSelectActive || pixelsPastView == 0) { // If band selection is inactive, or if it is active but not at the edge of the // view, no scrolling is necessary. mScrollStartTime = NOT_SET; return; } if (mScrollStartTime == NOT_SET) { // If the pointer was previously not at the edge of the view but now is, set the // start time for the scroll. mScrollStartTime = System.currentTimeMillis(); } // Compute the number of pixels to scroll, and scroll that many pixels. final int numPixels = computeNumPixelsToScroll( pixelsPastView, System.currentTimeMillis() - mScrollStartTime); mRecyclerView.scrollBy(0, numPixels); // Adjust the y-coordinate of the origin the opposite number of pixels so that the // origin remains in the same place relative to the view's items. mOrigin.y -= numPixels; resizeBandSelectRectangle(); mRecyclerView.removeCallbacks(mScrollRunnable); mRecyclerView.postOnAnimation(this); } /** * Computes the number of pixels to scroll based on how far the pointer is past the end of * the view and how long it has been there. Roughly based on ItemTouchHelper's algorithm for * computing the number of pixels to scroll when an item is dragged to the end of a * {@link RecyclerView}. * @param pixelsPastView * @param scrollDuration * @return */ private int computeNumPixelsToScroll(int pixelsPastView, long scrollDuration) { final int maxScrollStep = computeMaxScrollStep(mRecyclerView); final int direction = (int) Math.signum(pixelsPastView); final int absPastView = Math.abs(pixelsPastView); // Calculate the ratio of how far out of the view the pointer currently resides to the // entire height of the view. final float outOfBoundsRatio = Math.min( 1.0f, (float) absPastView / mRecyclerView.getHeight()); // Interpolate this ratio and use it to compute the maximum scroll that should be // possible for this step. final float cappedScrollStep = direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio); // Likewise, calculate the ratio of the time spent in the scroll to the limit. final float timeRatio = Math.min( 1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS); // Interpolate this ratio and use it to compute the final number of pixels to scroll. final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio)); // If the final number of pixels to scroll ends up being 0, the view should still scroll // at least one pixel. return numPixels != 0 ? numPixels : direction; } /** * Computes the maximum scroll allowed for a given animation frame. Currently, this * defaults to the height of the view, but this could be tweaked if this results in scrolls * that are too fast or too slow. * @param rv * @return */ private int computeMaxScrollStep(RecyclerView rv) { return rv.getHeight(); } /** * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends at * (1,1) and quickly approaches 1 near the start of that interval. This ensures that drags * that are at the edge or barely past the edge of the view still cause sufficient * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if needed. * @param ratio A ratio which is in the range [0, 1]. * @return A "smoothed" value, also in the range [0, 1]. */ private float smoothOutOfBoundsRatio(float ratio) { return (float) Math.pow(ratio - 1.0f, 5) + 1.0f; } /** * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1) and * stays close to 0 for most input values except those very close to 1. This ensures that * scrolls start out very slowly but speed up drastically after the scroll has been in * progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used, but this * could also be tweaked if needed. * @param ratio A ratio which is in the range [0, 1]. * @return A "smoothed" value, also in the range [0, 1]. */ private float smoothTimeRatio(float ratio) { return (float) Math.pow(ratio, 5); } }; /** * @param recyclerView * @param multiSelectManager */ public BandSelectManager(RecyclerView recyclerView, MultiSelectManager multiSelectManager) { mRecyclerView = recyclerView; mSelectManager = multiSelectManager; mRegionSelectorDrawable = mRecyclerView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay); mRecyclerView.addOnItemTouchListener(this); } @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { // Only intercept the event if it was triggered by a mouse. If band select is inactive, // do not intercept ACTION_UP events as they will not be processed. return isMouseEvent(e) && (mIsBandSelectActive || e.getActionMasked() != MotionEvent.ACTION_UP); } @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { checkState(isMouseEvent(e)); processMotionEvent(e); } /** * Processes a MotionEvent by starting, ending, or resizing the band select overlay. * @param e */ private void processMotionEvent(MotionEvent e) { if (mIsBandSelectActive && e.getActionMasked() == MotionEvent.ACTION_UP) { endBandSelect(); return; } mPointer = new Point((int) e.getX(), (int) e.getY()); if (!mIsBandSelectActive) { startBandSelect(); } scrollViewIfNecessary(); resizeBandSelectRectangle(); selectChildrenCoveredBySelection(); } /** * Starts band select by adding the drawable to the RecyclerView's overlay. */ private void startBandSelect() { if (DEBUG) Log.d(TAG, "Starting band select from (" + mPointer.x + "," + mPointer.y + ")."); mIsBandSelectActive = true; mOrigin = mPointer; mRecyclerView.getOverlay().add(mRegionSelectorDrawable); } /** * Scrolls the view if necessary. */ private void scrollViewIfNecessary() { mRecyclerView.removeCallbacks(mScrollRunnable); mScrollRunnable.run(); mRecyclerView.invalidate(); } /** * Resizes the band select rectangle by using the origin and the current pointer positoin as * two opposite corners of the selection. */ private void resizeBandSelectRectangle() { if (mBounds != null) { mCursorDeltaY = mPointer.y - mBounds.bottom; } mBounds = new Rect(Math.min(mOrigin.x, mPointer.x), Math.min(mOrigin.y, mPointer.y), Math.max(mOrigin.x, mPointer.x), Math.max(mOrigin.y, mPointer.y)); mRegionSelectorDrawable.setBounds(mBounds); } /** * Selects the children covered by the band select overlay by delegating to MultiSelectManager. * TODO: Provide a finished implementation. This is down and dirty, proof of concept code. * Final optimized implementation, with support for managing offscreen selection to come. */ private void selectChildrenCoveredBySelection() { // track top and bottom selections. Details on why this is useful below. int first = NOT_SELECTED; int last = NOT_SELECTED; for (int i = 0; i < mRecyclerView.getChildCount(); i++) { View child = mRecyclerView.getChildAt(i); ViewHolder holder = mRecyclerView.getChildViewHolder(child); Rect childRect = new Rect(); child.getHitRect(childRect); boolean shouldSelect = Rect.intersects(childRect, mBounds); int position = holder.getAdapterPosition(); // This also allows us to clear the selection of elements // that only temporarily entered the bounds of the band. if (mSelectedByBand.get(position) && !shouldSelect) { mSelectManager.setItemSelected(position, false); mSelectedByBand.delete(position); } // We need to keep track of the first and last items selected. // We'll use this information along with cursor direction // to determine the starting point of the selection. // We provide this information to selection manager // to enable more natural user interaction when working // with Shift+Click and multiple contiguous selection ranges. if (shouldSelect) { if (first == NOT_SELECTED) { first = position; } else { last = position; } mSelectManager.setItemSelected(position, true); mSelectedByBand.put(position, true); } } // Remember which is the last selected item, so we can // share that with selection manager when band select ends. // It'll use that as it's begin selection point when // user SHIFT+Clicks. if (mCursorDeltaY < 0 && last != NOT_SELECTED) { mFirstSelected = last; } else if (mCursorDeltaY > 0 && first != NOT_SELECTED) { mFirstSelected = first; } } /** * Ends band select by removing the overlay. */ private void endBandSelect() { if (DEBUG) Log.d(TAG, "Ending band select."); mIsBandSelectActive = false; mSelectedByBand.clear(); mRecyclerView.getOverlay().remove(mRegionSelectorDrawable); if (mFirstSelected != NOT_SELECTED) { mSelectManager.setSelectionFocusBegin(mFirstSelected); } } } Loading
packages/DocumentsUI/src/com/android/documentsui/BandSelectManager.javadeleted 100644 → 0 +0 −335 Original line number Diff line number Diff line /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui; import static com.android.documentsui.Events.isMouseEvent; import static com.android.internal.util.Preconditions.checkState; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.ViewHolder; import android.util.Log; import android.util.SparseBooleanArray; import android.view.MotionEvent; import android.view.View; /** * Provides mouse driven band-select support when used in conjuction with {@link RecyclerView} and * {@link MultiSelectManager}. This class is responsible for rendering the band select overlay and * selecting overlaid items via MultiSelectManager. */ public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener { private static final int NOT_SELECTED = -1; private static final int NOT_SET = -1; // For debugging purposes. private static final String TAG = "BandSelectManager"; private static final boolean DEBUG = false; private final RecyclerView mRecyclerView; private final MultiSelectManager mSelectManager; private final Drawable mRegionSelectorDrawable; private final SparseBooleanArray mSelectedByBand = new SparseBooleanArray(); private boolean mIsBandSelectActive = false; private Point mOrigin; private Point mPointer; private Rect mBounds; // Maintain the last selection made by band, so if bounds shrink back, we can deselect // the respective items. private int mCursorDeltaY = 0; private int mFirstSelected = NOT_SELECTED; // The time at which the current band selection-induced scroll began. If no scroll is in // progress, the value is NOT_SET. private long mScrollStartTime = NOT_SET; private final Runnable mScrollRunnable = new Runnable() { /** * The number of milliseconds of scrolling at which scroll speed continues to increase. At * first, the scroll starts slowly; then, the rate of scrolling increases until it reaches * its maximum value at after this many milliseconds. */ private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; @Override public void run() { // Compute the number of pixels the pointer's y-coordinate is past the view. Negative // values mean the pointer is at or before the top of the view, and positive values mean // that the pointer is at or after the bottom of the view. Note that one additional // pixel is added here so that the view still scrolls when the pointer is exactly at the // top or bottom. int pixelsPastView = 0; if (mPointer.y <= 0) { pixelsPastView = mPointer.y - 1; } else if (mPointer.y >= mRecyclerView.getHeight() - 1) { pixelsPastView = mPointer.y - mRecyclerView.getHeight() + 1; } if (!mIsBandSelectActive || pixelsPastView == 0) { // If band selection is inactive, or if it is active but not at the edge of the // view, no scrolling is necessary. mScrollStartTime = NOT_SET; return; } if (mScrollStartTime == NOT_SET) { // If the pointer was previously not at the edge of the view but now is, set the // start time for the scroll. mScrollStartTime = System.currentTimeMillis(); } // Compute the number of pixels to scroll, and scroll that many pixels. final int numPixels = computeNumPixelsToScroll( pixelsPastView, System.currentTimeMillis() - mScrollStartTime); mRecyclerView.scrollBy(0, numPixels); // Adjust the y-coordinate of the origin the opposite number of pixels so that the // origin remains in the same place relative to the view's items. mOrigin.y -= numPixels; resizeBandSelectRectangle(); mRecyclerView.removeCallbacks(mScrollRunnable); mRecyclerView.postOnAnimation(this); } /** * Computes the number of pixels to scroll based on how far the pointer is past the end of * the view and how long it has been there. Roughly based on ItemTouchHelper's algorithm for * computing the number of pixels to scroll when an item is dragged to the end of a * {@link RecyclerView}. * @param pixelsPastView * @param scrollDuration * @return */ private int computeNumPixelsToScroll(int pixelsPastView, long scrollDuration) { final int maxScrollStep = computeMaxScrollStep(mRecyclerView); final int direction = (int) Math.signum(pixelsPastView); final int absPastView = Math.abs(pixelsPastView); // Calculate the ratio of how far out of the view the pointer currently resides to the // entire height of the view. final float outOfBoundsRatio = Math.min( 1.0f, (float) absPastView / mRecyclerView.getHeight()); // Interpolate this ratio and use it to compute the maximum scroll that should be // possible for this step. final float cappedScrollStep = direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio); // Likewise, calculate the ratio of the time spent in the scroll to the limit. final float timeRatio = Math.min( 1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS); // Interpolate this ratio and use it to compute the final number of pixels to scroll. final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio)); // If the final number of pixels to scroll ends up being 0, the view should still scroll // at least one pixel. return numPixels != 0 ? numPixels : direction; } /** * Computes the maximum scroll allowed for a given animation frame. Currently, this * defaults to the height of the view, but this could be tweaked if this results in scrolls * that are too fast or too slow. * @param rv * @return */ private int computeMaxScrollStep(RecyclerView rv) { return rv.getHeight(); } /** * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends at * (1,1) and quickly approaches 1 near the start of that interval. This ensures that drags * that are at the edge or barely past the edge of the view still cause sufficient * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if needed. * @param ratio A ratio which is in the range [0, 1]. * @return A "smoothed" value, also in the range [0, 1]. */ private float smoothOutOfBoundsRatio(float ratio) { return (float) Math.pow(ratio - 1.0f, 5) + 1.0f; } /** * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1) and * stays close to 0 for most input values except those very close to 1. This ensures that * scrolls start out very slowly but speed up drastically after the scroll has been in * progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used, but this * could also be tweaked if needed. * @param ratio A ratio which is in the range [0, 1]. * @return A "smoothed" value, also in the range [0, 1]. */ private float smoothTimeRatio(float ratio) { return (float) Math.pow(ratio, 5); } }; /** * @param recyclerView * @param multiSelectManager */ public BandSelectManager(RecyclerView recyclerView, MultiSelectManager multiSelectManager) { mRecyclerView = recyclerView; mSelectManager = multiSelectManager; mRegionSelectorDrawable = mRecyclerView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay); mRecyclerView.addOnItemTouchListener(this); } @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { // Only intercept the event if it was triggered by a mouse. If band select is inactive, // do not intercept ACTION_UP events as they will not be processed. return isMouseEvent(e) && (mIsBandSelectActive || e.getActionMasked() != MotionEvent.ACTION_UP); } @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { checkState(isMouseEvent(e)); processMotionEvent(e); } /** * Processes a MotionEvent by starting, ending, or resizing the band select overlay. * @param e */ private void processMotionEvent(MotionEvent e) { if (mIsBandSelectActive && e.getActionMasked() == MotionEvent.ACTION_UP) { endBandSelect(); return; } mPointer = new Point((int) e.getX(), (int) e.getY()); if (!mIsBandSelectActive) { startBandSelect(); } scrollViewIfNecessary(); resizeBandSelectRectangle(); selectChildrenCoveredBySelection(); } /** * Starts band select by adding the drawable to the RecyclerView's overlay. */ private void startBandSelect() { if (DEBUG) Log.d(TAG, "Starting band select from (" + mPointer.x + "," + mPointer.y + ")."); mIsBandSelectActive = true; mOrigin = mPointer; mRecyclerView.getOverlay().add(mRegionSelectorDrawable); } /** * Scrolls the view if necessary. */ private void scrollViewIfNecessary() { mRecyclerView.removeCallbacks(mScrollRunnable); mScrollRunnable.run(); mRecyclerView.invalidate(); } /** * Resizes the band select rectangle by using the origin and the current pointer positoin as * two opposite corners of the selection. */ private void resizeBandSelectRectangle() { if (mBounds != null) { mCursorDeltaY = mPointer.y - mBounds.bottom; } mBounds = new Rect(Math.min(mOrigin.x, mPointer.x), Math.min(mOrigin.y, mPointer.y), Math.max(mOrigin.x, mPointer.x), Math.max(mOrigin.y, mPointer.y)); mRegionSelectorDrawable.setBounds(mBounds); } /** * Selects the children covered by the band select overlay by delegating to MultiSelectManager. * TODO: Provide a finished implementation. This is down and dirty, proof of concept code. * Final optimized implementation, with support for managing offscreen selection to come. */ private void selectChildrenCoveredBySelection() { // track top and bottom selections. Details on why this is useful below. int first = NOT_SELECTED; int last = NOT_SELECTED; for (int i = 0; i < mRecyclerView.getChildCount(); i++) { View child = mRecyclerView.getChildAt(i); ViewHolder holder = mRecyclerView.getChildViewHolder(child); Rect childRect = new Rect(); child.getHitRect(childRect); boolean shouldSelect = Rect.intersects(childRect, mBounds); int position = holder.getAdapterPosition(); // This also allows us to clear the selection of elements // that only temporarily entered the bounds of the band. if (mSelectedByBand.get(position) && !shouldSelect) { mSelectManager.setItemSelected(position, false); mSelectedByBand.delete(position); } // We need to keep track of the first and last items selected. // We'll use this information along with cursor direction // to determine the starting point of the selection. // We provide this information to selection manager // to enable more natural user interaction when working // with Shift+Click and multiple contiguous selection ranges. if (shouldSelect) { if (first == NOT_SELECTED) { first = position; } else { last = position; } mSelectManager.setItemSelected(position, true); mSelectedByBand.put(position, true); } } // Remember which is the last selected item, so we can // share that with selection manager when band select ends. // It'll use that as it's begin selection point when // user SHIFT+Clicks. if (mCursorDeltaY < 0 && last != NOT_SELECTED) { mFirstSelected = last; } else if (mCursorDeltaY > 0 && first != NOT_SELECTED) { mFirstSelected = first; } } /** * Ends band select by removing the overlay. */ private void endBandSelect() { if (DEBUG) Log.d(TAG, "Ending band select."); mIsBandSelectActive = false; mSelectedByBand.clear(); mRecyclerView.getOverlay().remove(mRegionSelectorDrawable); if (mFirstSelected != NOT_SELECTED) { mSelectManager.setSelectionFocusBegin(mFirstSelected); } } }