Merge "Allow drag-n-drop to auto-scroll when near top/bottom of dirlist." into nyc-andromeda-dev
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 5f1b349..7cda341 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 0cd568a..c1db87d 100644
--- a/src/com/android/documentsui/Shared.java
+++ b/src/com/android/documentsui/Shared.java
@@ -21,7 +21,6 @@
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.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 eb53ec1..5ab85c1 100644
--- a/src/com/android/documentsui/dirlist/BandController.java
+++ b/src/com/android/documentsui/dirlist/BandController.java
@@ -19,6 +19,7 @@
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.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 @@
*/
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 @@
@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 @@
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 @@
};
}
+ private boolean isActive() {
+ return mModel != null;
+ }
+
void bindSelection(Selection selection) {
mSelection = selection;
}
@@ -212,10 +234,6 @@
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 @@
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 @@
* 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 0860f4c..f0a7aae 100644
--- a/src/com/android/documentsui/dirlist/DirectoryDragListener.java
+++ b/src/com/android/documentsui/dirlist/DirectoryDragListener.java
@@ -45,4 +45,4 @@
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 ca7b2ca..2e1b1d6 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -74,7 +74,6 @@
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.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 @@
private @Nullable BandController mBandController;
private @Nullable ActionMode mActionMode;
- private DirectoryDragListener mOnDragListener;
+ private DragScrollListener mOnDragListener;
private MenuManager mMenuManager;
@Override
@@ -210,7 +210,8 @@
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 0000000..898a4a2
--- /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 0000000..5ef68ca
--- /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 0000000..e2aaa84
--- /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;
+ }
+ }
+}