Allow drag-n-drop to auto-scroll when near top/bottom of dirlist.

Couple of things this CL did:
1. Refactored ViewScroller to a separate file, renamed to
"ViewAutoScroller"
2. New interfaces called UIDelegate and CalculationDelegate such that
classes can provide necessary information for smooth scrolling
3. DirectoryDragListener implementation of #2's interface in order to
have auto scrolling when user's cursor is near top/bottom of screen

Currently since the status bar, action bar, and menu bar don't have a
drag listener, we don't get a callback on them, and thus we lose the
ability to get positions/scroll if user flings too quickly to those
areas. There might be some way to circumvent this since hovering over
these areas get a DRAG_EXIT event, but that should be 1) a separate
discussion and 2) a separate CL in my opinion.

Bug: 28696867
Change-Id: I61eba93aa60efd1638b3c0cd79db3f0d083b5e6d
(cherry picked from commit ad3a323585e43d06f632d85759beaf670f541997)
This commit is contained in:
Ben Lin
2016-07-13 18:16:36 -07:00
parent a01880be15
commit 3d9a06774c
8 changed files with 532 additions and 129 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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);

View File

@@ -45,4 +45,4 @@ class DirectoryDragListener extends ItemDragListener<DirectoryFragment> {
public boolean handleDropEventChecked(View v, DragEvent event) {
return mDragHost.handleDropEvent(v, event);
}
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}