1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui;
18 
19 import android.graphics.Point;
20 
21 /**
22  * Provides auto-scrolling upon request when user's interaction with the application
23  * introduces a natural intent to scroll. Used by DragHoverListener to allow auto scrolling
24  * when user either does band selection, attempting to drag and drop files to somewhere off
25  * the current screen, or trying to motion select past top/bottom of the screen.
26  */
27 public final class ViewAutoScroller implements Runnable {
28 
29     // ratio used to calculate the top/bottom hotspot region; used with view height
30     public static final float TOP_BOTTOM_THRESHOLD_RATIO = 0.125f;
31     public static final int MAX_SCROLL_STEP = 70;
32 
33     private ScrollHost mHost;
34     private ScrollerCallbacks mCallbacks;
35 
ViewAutoScroller(ScrollHost scrollHost, ScrollerCallbacks callbacks)36     public ViewAutoScroller(ScrollHost scrollHost, ScrollerCallbacks callbacks) {
37         assert scrollHost != null;
38         assert callbacks != null;
39 
40         mHost = scrollHost;
41         mCallbacks = callbacks;
42     }
43 
44     /**
45      * Attempts to smooth-scroll the view at the given UI frame. Application should be
46      * responsible to do any clean up (such as unsubscribing scrollListeners) after the run has
47      * finished, and re-run this method on the next UI frame if applicable.
48      */
49     @Override
run()50     public void run() {
51         // Compute the number of pixels the pointer's y-coordinate is past the view.
52         // Negative values mean the pointer is at or before the top of the view, and
53         // positive values mean that the pointer is at or after the bottom of the view. Note
54         // that top/bottom threshold is added here so that the view still scrolls when the
55         // pointer are in these buffer pixels.
56         int pixelsPastView = 0;
57 
58         final int topBottomThreshold = (int) (mHost.getViewHeight()
59                 * TOP_BOTTOM_THRESHOLD_RATIO);
60 
61         if (mHost.getCurrentPosition().y <= topBottomThreshold) {
62             pixelsPastView = mHost.getCurrentPosition().y - topBottomThreshold;
63         } else if (mHost.getCurrentPosition().y >= mHost.getViewHeight()
64                 - topBottomThreshold) {
65             pixelsPastView = mHost.getCurrentPosition().y - mHost.getViewHeight()
66                     + topBottomThreshold;
67         }
68 
69         if (!mHost.isActive() || pixelsPastView == 0) {
70             // If the operation that started the scrolling is no longer inactive, or if it is active
71             // but not at the edge of the view, no scrolling is necessary.
72             return;
73         }
74 
75         if (pixelsPastView > topBottomThreshold) {
76             pixelsPastView = topBottomThreshold;
77         }
78 
79         // Compute the number of pixels to scroll, and scroll that many pixels.
80         final int numPixels = computeScrollDistance(pixelsPastView);
81         mCallbacks.scrollBy(numPixels);
82 
83         // Remove callback to this, and then properly run at next frame again
84         mCallbacks.removeCallback(this);
85         mCallbacks.runAtNextFrame(this);
86     }
87 
88     /**
89      * Computes the number of pixels to scroll based on how far the pointer is past the end
90      * of the region. Roughly based on ItemTouchHelper's algorithm for computing the number of
91      * pixels to scroll when an item is dragged to the end of a view.
92      * @return
93      */
computeScrollDistance(int pixelsPastView)94     public int computeScrollDistance(int pixelsPastView) {
95         final int topBottomThreshold =
96                 (int) (mHost.getViewHeight() * TOP_BOTTOM_THRESHOLD_RATIO);
97 
98         final int direction = (int) Math.signum(pixelsPastView);
99         final int absPastView = Math.abs(pixelsPastView);
100 
101         // Calculate the ratio of how far out of the view the pointer currently resides to
102         // the top/bottom scrolling hotspot of the view.
103         final float outOfBoundsRatio = Math.min(
104                 1.0f, (float) absPastView / topBottomThreshold);
105         // Interpolate this ratio and use it to compute the maximum scroll that should be
106         // possible for this step.
107         final int cappedScrollStep =
108                 (int) (direction * MAX_SCROLL_STEP * smoothOutOfBoundsRatio(outOfBoundsRatio));
109 
110         // If the final number of pixels to scroll ends up being 0, the view should still
111         // scroll at least one pixel.
112         return cappedScrollStep != 0 ? cappedScrollStep : direction;
113     }
114 
115     /**
116      * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
117      * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
118      * drags that are at the edge or barely past the edge of the threshold does little to no
119      * scrolling, while drags that are near the edge of the view does a lot of
120      * scrolling. The equation y=x^10 is used, but this could also be tweaked if
121      * needed.
122      * @param ratio A ratio which is in the range [0, 1].
123      * @return A "smoothed" value, also in the range [0, 1].
124      */
smoothOutOfBoundsRatio(float ratio)125     private float smoothOutOfBoundsRatio(float ratio) {
126         return (float) Math.pow(ratio, 10);
127     }
128 
129     /**
130      * Used by {@link run} to properly calculate the proper amount of pixels to scroll given time
131      * passed since scroll started, and to properly scroll / proper listener clean up if necessary.
132      */
133     public static abstract class ScrollHost {
getCurrentPosition()134         public abstract Point getCurrentPosition();
getViewHeight()135         public abstract int getViewHeight();
isActive()136         public abstract boolean isActive();
137     }
138 
139     /**
140      * Callback used by scroller to perform UI tasks, such as scrolling and rerunning at next UI
141      * cycle.
142      */
143     public static abstract class ScrollerCallbacks {
scrollBy(int dy)144         public void scrollBy(int dy) {}
runAtNextFrame(Runnable r)145         public void runAtNextFrame(Runnable r) {}
removeCallback(Runnable r)146         public void removeCallback(Runnable r) {}
147     }
148 }
149