diff options
author | 2016-07-22 17:00:07 +0000 | |
---|---|---|
committer | 2016-07-22 17:00:08 +0000 | |
commit | 31d9b44b3f7a96c5d907a025a63235415e04bc19 (patch) | |
tree | 2da23cb6a9e80c303a3af11cc956868be67752c7 | |
parent | 0f8e3cde86b6385cdacd29c0644c7f610a4c2d5a (diff) | |
parent | c5e3e8eb996187ba780ef8654e66a58b940dcb40 (diff) |
Merge "Allow drag-n-drop to auto-scroll when near top/bottom of dirlist." into nyc-andromeda-dev
8 files changed, 532 insertions, 129 deletions
diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 5f1b349d5..7cda34101 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -46,4 +46,6 @@ <dimen name="drag_shadow_width">160dp</dimen> <dimen name="drag_shadow_height">48dp</dimen> + <dimen name="autoscroll_edge_height">32dp</dimen> + </resources> diff --git a/src/com/android/documentsui/Shared.java b/src/com/android/documentsui/Shared.java index 0cd568a72..c1db87d50 100644 --- a/src/com/android/documentsui/Shared.java +++ b/src/com/android/documentsui/Shared.java @@ -21,7 +21,6 @@ import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; -import android.net.Uri; import android.os.Looper; import android.provider.DocumentsContract; import android.text.TextUtils; @@ -30,10 +29,6 @@ import android.text.format.Time; import android.util.Log; import android.view.WindowManager; -import com.android.documentsui.model.DocumentInfo; -import com.android.documentsui.model.RootInfo; - -import java.io.FileNotFoundException; import java.text.Collator; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/documentsui/dirlist/BandController.java b/src/com/android/documentsui/dirlist/BandController.java index eb53ec1a4..5ab85c1fc 100644 --- a/src/com/android/documentsui/dirlist/BandController.java +++ b/src/com/android/documentsui/dirlist/BandController.java @@ -19,6 +19,7 @@ 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 static com.android.documentsui.dirlist.ViewAutoScroller.NOT_SET; import android.graphics.Point; import android.graphics.Rect; @@ -39,6 +40,8 @@ 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 com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate; +import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate; import java.util.ArrayList; import java.util.Collections; @@ -54,15 +57,14 @@ import java.util.Set; */ public class BandController extends RecyclerView.OnScrollListener { - private static final int NOT_SET = -1; - private static final String TAG = "BandController"; + private static final int AUTOSCROLL_EDGE_HEIGHT = 1; private final Runnable mModelBuilder; private final SelectionEnvironment mEnvironment; private final DocumentsAdapter mAdapter; private final MultiSelectManager mSelectionManager; - private final Runnable mViewScroller = new ViewScroller(); + private final Runnable mViewScroller; private final GridModel.OnSelectionChangedListener mGridListener; @Nullable private Rect mBounds; @@ -70,9 +72,6 @@ public class BandController extends RecyclerView.OnScrollListener { @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( @@ -114,6 +113,25 @@ public class BandController extends RecyclerView.OnScrollListener { mSelectionManager = selectionManager; mEnvironment.addOnScrollListener(this); + mViewScroller = new ViewAutoScroller( + AUTOSCROLL_EDGE_HEIGHT, + new ScrollDistanceDelegate() { + @Override + public Point getCurrentPosition() { + return mCurrentPosition; + } + + @Override + public int getViewHeight() { + return mEnvironment.getHeight(); + } + + @Override + public boolean isActive() { + return BandController.this.isActive(); + } + }, + env); mAdapter.registerAdapterDataObserver( new RecyclerView.AdapterDataObserver() { @@ -173,6 +191,10 @@ public class BandController extends RecyclerView.OnScrollListener { }; } + private boolean isActive() { + return mModel != null; + } + void bindSelection(Selection selection) { mSelection = selection; } @@ -212,10 +234,6 @@ public class BandController extends RecyclerView.OnScrollListener { 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. @@ -336,112 +354,6 @@ public class BandController extends RecyclerView.OnScrollListener { 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()) { @@ -1110,16 +1022,13 @@ public class BandController extends RecyclerView.OnScrollListener { * Provides functionality for BandController. Exists primarily to tests that are * fully isolated from RecyclerView. */ - interface SelectionEnvironment { + interface SelectionEnvironment extends ScrollActionDelegate { 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); diff --git a/src/com/android/documentsui/dirlist/DirectoryDragListener.java b/src/com/android/documentsui/dirlist/DirectoryDragListener.java index 0860f4ca1..f0a7aae2b 100644 --- a/src/com/android/documentsui/dirlist/DirectoryDragListener.java +++ b/src/com/android/documentsui/dirlist/DirectoryDragListener.java @@ -45,4 +45,4 @@ class DirectoryDragListener extends ItemDragListener<DirectoryFragment> { public boolean handleDropEventChecked(View v, DragEvent event) { return mDragHost.handleDropEvent(v, event); } -} +}
\ No newline at end of file diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java index ca7b2ca6a..2e1b1d67e 100644 --- a/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -74,7 +74,6 @@ import android.widget.Toolbar; import com.android.documentsui.BaseActivity; import com.android.documentsui.DirectoryLoader; import com.android.documentsui.DirectoryResult; -import com.android.documentsui.clipping.DocumentClipper; import com.android.documentsui.DocumentsActivity; import com.android.documentsui.DocumentsApplication; import com.android.documentsui.Events.InputEvent; @@ -93,6 +92,7 @@ import com.android.documentsui.Shared; import com.android.documentsui.Snackbars; import com.android.documentsui.State; import com.android.documentsui.State.ViewMode; +import com.android.documentsui.clipping.DocumentClipper; import com.android.documentsui.clipping.UrisSupplier; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.dirlist.UserInputHandler.DocumentDetails; @@ -183,7 +183,7 @@ public class DirectoryFragment extends Fragment private @Nullable BandController mBandController; private @Nullable ActionMode mActionMode; - private DirectoryDragListener mOnDragListener; + private DragScrollListener mOnDragListener; private MenuManager mMenuManager; @Override @@ -210,7 +210,8 @@ public class DirectoryFragment extends Fragment mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity())); - mOnDragListener = new DirectoryDragListener(this); + mOnDragListener = DragScrollListener.create( + getActivity(), new DirectoryDragListener(this), mRecView); // Make the recycler and the empty views responsive to drop events. mRecView.setOnDragListener(mOnDragListener); diff --git a/src/com/android/documentsui/dirlist/DragScrollListener.java b/src/com/android/documentsui/dirlist/DragScrollListener.java new file mode 100644 index 000000000..898a4a23a --- /dev/null +++ b/src/com/android/documentsui/dirlist/DragScrollListener.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2016 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 android.content.Context; +import android.graphics.Point; +import android.view.DragEvent; +import android.view.View; +import android.view.View.OnDragListener; + +import com.android.documentsui.ItemDragListener; +import com.android.documentsui.ItemDragListener.DragHost; +import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate; +import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate; +import com.android.documentsui.R; + +import java.util.function.BooleanSupplier; +import java.util.function.IntSupplier; + +import javax.annotation.Nullable; + +/** + * This class acts as a middle-man handler for potential auto-scrolling before passing the dragEvent + * onto {@link DirectoryDragListener}. + */ +class DragScrollListener implements OnDragListener { + + private final ItemDragListener<? extends DragHost> mDragHandler; + private final IntSupplier mHeight; + private final BooleanSupplier mCanScrollUp; + private final BooleanSupplier mCanScrollDown; + private final int mAutoScrollEdgeHeight; + private final Runnable mDragScroller; + + private boolean mDragHappening; + private @Nullable Point mCurrentPosition; + + private DragScrollListener( + Context context, + ItemDragListener<? extends DragHost> dragHandler, + IntSupplier heightSupplier, + BooleanSupplier scrollUpSupplier, + BooleanSupplier scrollDownSupplier, + ViewAutoScroller.ScrollActionDelegate actionDelegate) { + mDragHandler = dragHandler; + mAutoScrollEdgeHeight = (int) context.getResources() + .getDimension(R.dimen.autoscroll_edge_height); + mHeight = heightSupplier; + mCanScrollUp = scrollUpSupplier; + mCanScrollDown = scrollDownSupplier; + + ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() { + @Override + public Point getCurrentPosition() { + return mCurrentPosition; + } + + @Override + public int getViewHeight() { + return mHeight.getAsInt(); + } + + @Override + public boolean isActive() { + return mDragHappening; + } + }; + + mDragScroller = new ViewAutoScroller( + mAutoScrollEdgeHeight, distanceDelegate, actionDelegate); + } + + static DragScrollListener create( + Context context, ItemDragListener<? extends DragHost> dragHandler, View scrollView) { + ScrollActionDelegate actionDelegate = new ScrollActionDelegate() { + @Override + public void scrollBy(int dy) { + scrollView.scrollBy(0, dy); + } + + @Override + public void runAtNextFrame(Runnable r) { + scrollView.postOnAnimation(r); + + } + + @Override + public void removeCallback(Runnable r) { + scrollView.removeCallbacks(r); + } + }; + DragScrollListener listener = new DragScrollListener( + context, + dragHandler, + scrollView::getHeight, + () -> { + return scrollView.canScrollVertically(-1); + }, + () -> { + return scrollView.canScrollVertically(1); + }, + actionDelegate); + return listener; + } + + @Override + public boolean onDrag(View v, DragEvent event) { + boolean handled = false; + switch (event.getAction()) { + case DragEvent.ACTION_DRAG_STARTED: + mDragHappening = true; + break; + case DragEvent.ACTION_DRAG_ENDED: + mDragHappening = false; + break; + case DragEvent.ACTION_DRAG_ENTERED: + handled = insideDragZone(); + break; + case DragEvent.ACTION_DRAG_LOCATION: + handled = handleLocationEvent(v, event.getX(), event.getY()); + break; + default: + break; + } + + if (!handled) { + handled = mDragHandler.onDrag(v, event); + } + + return handled; + } + + private boolean handleLocationEvent(View v, float x, float y) { + mCurrentPosition = new Point(Math.round(v.getX() + x), Math.round(v.getY() + y)); + if (insideDragZone()) { + mDragScroller.run(); + return true; + } + return false; + } + + private boolean insideDragZone() { + if (mCurrentPosition == null) { + return false; + } + + boolean shouldScrollUp = mCurrentPosition.y < mAutoScrollEdgeHeight + && mCanScrollUp.getAsBoolean(); + boolean shouldScrollDown = mCurrentPosition.y > mHeight.getAsInt() - mAutoScrollEdgeHeight + && mCanScrollDown.getAsBoolean(); + return shouldScrollUp || shouldScrollDown; + } +}
\ No newline at end of file diff --git a/src/com/android/documentsui/dirlist/ViewAutoScroller.java b/src/com/android/documentsui/dirlist/ViewAutoScroller.java new file mode 100644 index 000000000..5ef68ca79 --- /dev/null +++ b/src/com/android/documentsui/dirlist/ViewAutoScroller.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2016 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 android.graphics.Point; +import android.support.annotation.VisibleForTesting; + +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; + +/** + * Provides auto-scrolling upon request when user's interaction with the application + * introduces a natural intent to scroll. Used by {@link BandController} and + * {@link DragScrollListener} to allow auto scrolling when user either does band selection, or + * attempting to drag and drop files to somewhere off the current screen. + */ +public final class ViewAutoScroller implements Runnable { + public static final int NOT_SET = -1; + /** + * 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; + + // Top and bottom inner buffer such that user's cursor does not have to be exactly off screen + // for auto scrolling to begin + private final int mTopBottomThreshold; + private final ScrollDistanceDelegate mCalcDelegate; + private final ScrollActionDelegate mUiDelegate; + private final LongSupplier mCurrentTime; + + private long mScrollStartTime = NOT_SET; + + public ViewAutoScroller( + int topBottomThreshold, + ScrollDistanceDelegate calcDelegate, + ScrollActionDelegate uiDelegate) { + this(topBottomThreshold, calcDelegate, uiDelegate, System::currentTimeMillis); + } + + @VisibleForTesting + ViewAutoScroller( + int topBottomThreshold, + ScrollDistanceDelegate calcDelegate, + ScrollActionDelegate uiDelegate, + LongSupplier clock) { + mTopBottomThreshold = topBottomThreshold; + mCalcDelegate = calcDelegate; + mUiDelegate = uiDelegate; + mCurrentTime = clock; + } + + /** + * Attempts to smooth-scroll the view at the given UI frame. Application should be + * responsible to do any clean up (such as unsubscribing scrollListeners) after the run has + * finished, and re-run this method on the next UI frame if applicable. + */ + @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 top/bottom threshold is added here so that the view still scrolls when the + // pointer are in these buffer pixels. + int pixelsPastView = 0; + + if (mCalcDelegate.getCurrentPosition().y <= mTopBottomThreshold) { + pixelsPastView = mCalcDelegate.getCurrentPosition().y - mTopBottomThreshold; + } else if (mCalcDelegate.getCurrentPosition().y >= mCalcDelegate.getViewHeight() + - mTopBottomThreshold) { + pixelsPastView = mCalcDelegate.getCurrentPosition().y - mCalcDelegate.getViewHeight() + + mTopBottomThreshold; + } + + if (!mCalcDelegate.isActive() || pixelsPastView == 0) { + // If the operation that started the scrolling is no longer 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 = mCurrentTime.getAsLong(); + } + + // Compute the number of pixels to scroll, and scroll that many pixels. + final int numPixels = computeScrollDistance( + pixelsPastView, mCurrentTime.getAsLong() - mScrollStartTime); + mUiDelegate.scrollBy(numPixels); + + // Remove callback to this, and then properly run at next frame again + mUiDelegate.removeCallback(this); + mUiDelegate.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 view. + * @param pixelsPastView + * @param scrollDuration + * @return + */ + public int computeScrollDistance(int pixelsPastView, long scrollDuration) { + final int maxScrollStep = mCalcDelegate.getViewHeight(); + 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 / mCalcDelegate.getViewHeight()); + // 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); + } + + /** + * Used by {@link run} to properly calculate the proper amount of pixels to scroll given time + * passed since scroll started, and to properly scroll / proper listener clean up if necessary. + */ + interface ScrollDistanceDelegate { + public Point getCurrentPosition(); + public int getViewHeight(); + public boolean isActive(); + } + + /** + * Used by {@link run} to do UI tasks, such as scrolling and rerunning at next UI cycle. + */ + interface ScrollActionDelegate { + public void scrollBy(int dy); + public void runAtNextFrame(Runnable r); + public void removeCallback(Runnable r); + } +}
\ No newline at end of file diff --git a/tests/src/com/android/documentsui/dirlist/ViewAutoScrollerTest.java b/tests/src/com/android/documentsui/dirlist/ViewAutoScrollerTest.java new file mode 100644 index 000000000..e2aaa843e --- /dev/null +++ b/tests/src/com/android/documentsui/dirlist/ViewAutoScrollerTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2016 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 org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.graphics.Point; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.function.IntConsumer; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class ViewAutoScrollerTest { + + private static final int VIEW_HEIGHT = 100; + private static final int EDGE_HEIGHT = 10; + + private ViewAutoScroller mAutoScroller; + private Point mPoint; + private boolean mActive; + private ViewAutoScroller.ScrollDistanceDelegate mDistanceDelegate; + private ViewAutoScroller.ScrollActionDelegate mActionDelegate; + private IntConsumer mScrollAssert; + + @Before + public void setUp() { + mActive = false; + mPoint = new Point(); + mDistanceDelegate = new ViewAutoScroller.ScrollDistanceDelegate() { + @Override + public boolean isActive() { + return mActive; + } + + @Override + public int getViewHeight() { + return VIEW_HEIGHT; + } + + @Override + public Point getCurrentPosition() { + return mPoint; + } + }; + mActionDelegate = new ViewAutoScroller.ScrollActionDelegate() { + @Override + public void scrollBy(int dy) { + mScrollAssert.accept(dy); + } + + @Override + public void runAtNextFrame(Runnable r) { + } + + @Override + public void removeCallback(Runnable r) { + } + }; + mAutoScroller = new ViewAutoScroller( + EDGE_HEIGHT, mDistanceDelegate, mActionDelegate, new TestClock()::getCurrentTime); + } + + @Test + public void testCursorNotInScrollZone() { + mPoint = new Point(0, VIEW_HEIGHT/2); + mScrollAssert = (int dy) -> { + // Should not have called this method + fail("Received unexpected scroll event"); + assertTrue(false); + }; + mAutoScroller.run(); + } + + @Test + public void testCursorInScrollZone_notActive() { + mActive = false; + mPoint = new Point(0, EDGE_HEIGHT - 1); + mScrollAssert = (int dy) -> { + // Should not have called this method + fail("Received unexpected scroll event"); + assertTrue(false); + }; + mAutoScroller.run(); + } + + @Test + public void testCursorInScrollZone_top() { + mActive = true; + mPoint = new Point(0, EDGE_HEIGHT - 1); + int expectedScrollDistance = mAutoScroller.computeScrollDistance(-1, 1); + mScrollAssert = (int dy) -> { + assertTrue(dy == expectedScrollDistance); + }; + mAutoScroller.run(); + } + + @Test + public void testCursorInScrollZone_bottom() { + mActive = true; + mPoint = new Point(0, VIEW_HEIGHT - EDGE_HEIGHT + 1); + int expectedScrollDistance = mAutoScroller.computeScrollDistance(1, 1); + mScrollAssert = (int dy) -> { + assertTrue(dy == expectedScrollDistance); + }; + mAutoScroller.run(); + } + + class TestClock { + private int timesCalled = 0; + + public long getCurrentTime() { + return ++timesCalled; + } + } +} |