summaryrefslogtreecommitdiff
path: root/src/com/android/documentsui/ViewAutoScroller.java
blob: f7b062e186192ab8efdbd72358c387f2ab966f32 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/*
 * 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;

import android.graphics.Point;

/**
 * Provides auto-scrolling upon request when user's interaction with the application
 * introduces a natural intent to scroll. Used by DragHoverListener to allow auto scrolling
 * when user either does band selection, attempting to drag and drop files to somewhere off
 * the current screen, or trying to motion select past top/bottom of the screen.
 */
public final class ViewAutoScroller implements Runnable {

    // ratio used to calculate the top/bottom hotspot region; used with view height
    public static final float TOP_BOTTOM_THRESHOLD_RATIO = 0.125f;
    public static final int MAX_SCROLL_STEP = 70;

    private ScrollHost mHost;
    private ScrollerCallbacks mCallbacks;

    public ViewAutoScroller(ScrollHost scrollHost, ScrollerCallbacks callbacks) {
        assert scrollHost != null;
        assert callbacks != null;

        mHost = scrollHost;
        mCallbacks = callbacks;
    }

    /**
     * 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;

        final int topBottomThreshold = (int) (mHost.getViewHeight()
                * TOP_BOTTOM_THRESHOLD_RATIO);

        if (mHost.getCurrentPosition().y <= topBottomThreshold) {
            pixelsPastView = mHost.getCurrentPosition().y - topBottomThreshold;
        } else if (mHost.getCurrentPosition().y >= mHost.getViewHeight()
                - topBottomThreshold) {
            pixelsPastView = mHost.getCurrentPosition().y - mHost.getViewHeight()
                    + topBottomThreshold;
        }

        if (!mHost.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.
            return;
        }

        if (pixelsPastView > topBottomThreshold) {
            pixelsPastView = topBottomThreshold;
        }

        // Compute the number of pixels to scroll, and scroll that many pixels.
        final int numPixels = computeScrollDistance(pixelsPastView);
        mCallbacks.scrollBy(numPixels);

        // Remove callback to this, and then properly run at next frame again
        mCallbacks.removeCallback(this);
        mCallbacks.runAtNextFrame(this);
    }

    /**
     * Computes the number of pixels to scroll based on how far the pointer is past the end
     * of the region. 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.
     * @return
     */
    public int computeScrollDistance(int pixelsPastView) {
        final int topBottomThreshold =
                (int) (mHost.getViewHeight() * TOP_BOTTOM_THRESHOLD_RATIO);

        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 top/bottom scrolling hotspot of the view.
        final float outOfBoundsRatio = Math.min(
                1.0f, (float) absPastView / topBottomThreshold);
        // Interpolate this ratio and use it to compute the maximum scroll that should be
        // possible for this step.
        final int cappedScrollStep =
                (int) (direction * MAX_SCROLL_STEP * smoothOutOfBoundsRatio(outOfBoundsRatio));

        // If the final number of pixels to scroll ends up being 0, the view should still
        // scroll at least one pixel.
        return cappedScrollStep != 0 ? cappedScrollStep : 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 threshold does little to no
     * scrolling, while drags that are near the edge of the view does a lot of
     * scrolling. The equation y=x^10 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, 10);
    }

    /**
     * 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.
     */
    public static abstract class ScrollHost {
        public abstract Point getCurrentPosition();
        public abstract int getViewHeight();
        public abstract boolean isActive();
    }

    /**
     * Callback used by scroller to perform UI tasks, such as scrolling and rerunning at next UI
     * cycle.
     */
    public static abstract class ScrollerCallbacks {
        public void scrollBy(int dy) {}
        public void runAtNextFrame(Runnable r) {}
        public void removeCallback(Runnable r) {}
    }
}