summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/AutoScroller.java
blob: ac8e2e61a4906ed22ee23182998259d237289070 (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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
/*
 * Copyright (C) 2013 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.launcher3;

import android.graphics.Rect;
import android.graphics.RectF;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.AbsListView;

class AutoScroller implements View.OnTouchListener, Runnable {
    private static final int SCALE_RELATIVE = 0;
    private static final int SCALE_ABSOLUTE = 1;

    private final View mTarget;
    private final RampUpScroller mScroller;

    /** Interpolator used to scale velocity with touch position, may be null. */
    private Interpolator mEdgeInterpolator = new AccelerateInterpolator();

    /**
     * Type of maximum velocity scaling to use, one of:
     * <ul>
     * <li>{@link #SCALE_RELATIVE}
     * <li>{@link #SCALE_ABSOLUTE}
     * </ul>
     */
    private int mMaxVelocityScale = SCALE_RELATIVE;

    /**
     * Type of activation edge scaling to use, one of:
     * <ul>
     * <li>{@link #SCALE_RELATIVE}
     * <li>{@link #SCALE_ABSOLUTE}
     * </ul>
     */
    private int mActivationEdgeScale = SCALE_RELATIVE;

    /** Edge insets used to activate auto-scrolling. */
    private RectF mActivationEdges = new RectF(0.2f, 0.2f, 0.2f, 0.2f);

    /** Delay after entering an activation edge before auto-scrolling begins. */
    private int mActivationDelay;

    /** Maximum horizontal scrolling velocity. */
    private float mMaxVelocityX = 0.001f;

    /** Maximum vertical scrolling velocity. */
    private float mMaxVelocityY = 0.001f;

    /**
     * Whether positive insets should also extend beyond the view bounds when
     * auto-scrolling is already active. This allows a user to start scrolling
     * at an inside edge, then move beyond the edge and continue scrolling.
     */
    private boolean mExtendsBeyondEdges = true;

    /** Whether to start activation immediately. */
    private boolean mSkipDelay;

    /** Whether to reset the scroller start time on the next animation. */
    private boolean mResetScroller;

    /** Whether the auto-scroller is active. */
    private boolean mActive;
    private long[] mScrollStart = new long[2];

    /**
     * If the event is within this percentage of the edge of the scrolling area,
     * use accelerated scrolling.
     */
    private float mFastScrollingRange = 0.8f;

    /**
     * Duration of time spent in accelerated scrolling area before reaching
     * maximum velocity
     */
    private float mDurationToMax = 2500f;

    private static final int X = 0;
    private static final int Y = 1;

    public AutoScroller(View target) {
        mTarget = target;
        mScroller = new RampUpScroller(250);
        mActivationDelay = ViewConfiguration.getTapTimeout();
    }

    /**
     * Sets the maximum scrolling velocity as a fraction of the host view size
     * per second. For example, a maximum Y velocity of 1 would scroll one
     * vertical page per second. By default, both values are 1.
     *
     * @param x The maximum X velocity as a fraction of the host view width per
     *            second.
     * @param y The maximum Y velocity as a fraction of the host view height per
     *            second.
     */
    public void setMaximumVelocityRelative(float x, float y) {
        mMaxVelocityScale = SCALE_RELATIVE;
        mMaxVelocityX = x / 1000f;
        mMaxVelocityY = y / 1000f;
    }

    /**
     * Sets the maximum scrolling velocity as an absolute pixel distance per
     * second. For example, a maximum Y velocity of 100 would scroll one hundred
     * pixels per second.
     *
     * @param x The maximum X velocity as a fraction of the host view width per
     *            second.
     * @param y The maximum Y velocity as a fraction of the host view height per
     *            second.
     */
    public void setMaximumVelocityAbsolute(float x, float y) {
        mMaxVelocityScale = SCALE_ABSOLUTE;
        mMaxVelocityX = x / 1000f;
        mMaxVelocityY = y / 1000f;
    }

    /**
     * Sets the delay after entering an activation edge before activation of
     * auto-scrolling. By default, the activation delay is set to
     * {@link ViewConfiguration#getTapTimeout()}.
     *
     * @param delayMillis The delay in milliseconds.
     */
    public void setActivationDelay(int delayMillis) {
        mActivationDelay = delayMillis;
    }

    /**
     * Sets the activation edges in pixels. Edges are treated as insets, so
     * positive values expand into the view bounds while negative values extend
     * outside the bounds.
     *
     * @param l The left activation edge, in pixels.
     * @param t The top activation edge, in pixels.
     * @param r The right activation edge, in pixels.
     * @param b The bottom activation edge, in pixels.
     */
    public void setEdgesAbsolute(int l, int t, int r, int b) {
        mActivationEdgeScale = SCALE_ABSOLUTE;
        mActivationEdges.set(l, t, r, b);
    }

    /**
     * Whether positive insets should also extend beyond the view bounds when
     * auto-scrolling is already active. This allows a user to start scrolling
     * at an inside edge, then move beyond the edge and continue scrolling.
     *
     * @param e
     */
    public void setExtendsBeyondEdges(boolean e) {
        mExtendsBeyondEdges = e;
    }

    /**
     * Sets the activation edges as fractions of the host view size. Edges are
     * treated as insets, so positive values expand into the view bounds while
     * negative values extend outside the bounds. By default, all values are
     * 0.25.
     *
     * @param l The left activation edge, as a fraction of view size.
     * @param t The top activation edge, as a fraction of view size.
     * @param r The right activation edge, as a fraction of view size.
     * @param b The bottom activation edge, as a fraction of view size.
     */
    public void setEdgesRelative(float l, float t, float r, float b) {
        mActivationEdgeScale = SCALE_RELATIVE;
        mActivationEdges.set(l, t, r, b);
    }

    /**
     * Sets the {@link Interpolator} used for scaling touches within activation
     * edges. By default, uses the {@link AccelerateInterpolator} to gradually
     * speed up scrolling.
     *
     * @param edgeInterpolator The interpolator to use for activation edges, or
     *            {@code null} to use a fixed velocity during auto-scrolling.
     */
    public void setEdgeInterpolator(Interpolator edgeInterpolator) {
        mEdgeInterpolator = edgeInterpolator;
    }

    /**
     * Stop tracking scrolling.
     */
    public void stop() {
        stop(true);
    }

    /**
     * Pass the rectangle defining the drawing region for the object used to
     * trigger drag scrolling.
     *
     * @param v View on which the scrolling regions are defined
     * @param r Rect defining the drawing bounds of the object being dragged
     * @return whether the event was handled
     */
    public boolean onTouch(View v, Rect r) {
        MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
                SystemClock.uptimeMillis(), MotionEvent.ACTION_MOVE, r.left, r.top, 0);
        return onTouch(v, event);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        final int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                final int sourceWidth = v.getWidth();
                final int sourceHeight = v.getHeight();
                final float x = event.getX();
                final float y = event.getY();
                final float l;
                final float t;
                final float r;
                final float b;
                final RectF activationEdges = mActivationEdges;
                if (mActivationEdgeScale == SCALE_ABSOLUTE) {
                    l = activationEdges.left;
                    t = activationEdges.top;
                    r = activationEdges.right;
                    b = activationEdges.bottom;
                } else {
                    l = activationEdges.left * sourceWidth;
                    t = activationEdges.top * sourceHeight;
                    r = activationEdges.right * sourceWidth;
                    b = activationEdges.bottom * sourceHeight;
                }

                final float maxVelX;
                final float maxVelY;
                if (mMaxVelocityScale == SCALE_ABSOLUTE) {
                    maxVelX = mMaxVelocityX;
                    maxVelY = mMaxVelocityY;
                } else {
                    maxVelX = mMaxVelocityX * mTarget.getWidth();
                    maxVelY = mMaxVelocityY * mTarget.getHeight();
                }

                final float velocityX = getEdgeVelocity(X, l, r, x, sourceWidth, event);
                final float velocityY = getEdgeVelocity(Y, t, b, y, sourceHeight, event);
                mScroller.setTargetVelocity(velocityX * maxVelX, velocityY * maxVelY);

                if ((velocityX != 0 || velocityY != 0) && !mActive) {
                    mActive = true;
                    mResetScroller = true;
                    if (mSkipDelay) {
                        mTarget.postOnAnimation(this);
                    } else {
                        mSkipDelay = true;
                        mTarget.postOnAnimationDelayed(this, mActivationDelay);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                stop(true);
                break;
        }

        return false;
    }

    /**
     * @param leading Size of the leading activation inset.
     * @param trailing Size of the trailing activation inset.
     * @param current Position within within the total area.
     * @param size Size of the total area.
     * @return The fraction of the activation area.
     */
    private float getEdgeVelocity(int dir, float leading, float trailing,
            float current, float size, MotionEvent ev) {
        float valueLeading = 0;
        if (leading > 0) {
            if (current < leading) {
                if (current > 0) {
                    // Movement up to the edge is scaled.
                    valueLeading = 1f - current / leading;
                } else if (mActive && mExtendsBeyondEdges) {
                    // Movement beyond the edge is always maximum.
                    valueLeading = 1f;
                }
            }
        } else if (leading < 0) {
            if (current < 0) {
                // Movement beyond the edge is scaled.
                valueLeading = current / leading;
            }
        }

        float valueTrailing = 0;
        if (trailing > 0) {
            if (current > size - trailing) {
                if (current < size) {
                    // Movement up to the edge is scaled.
                    valueTrailing = 1f - (size - current) / trailing;
                } else if (mActive && mExtendsBeyondEdges) {
                    // Movement beyond the edge is always maximum.
                    valueTrailing = 1f;
                }
            }
        } else if (trailing < 0) {
            if (current > size) {
                // Movement beyond the edge is scaled.
                valueTrailing = (size - current) / trailing;
            }
        }

        float value = (valueTrailing - valueLeading);
        if ((value > mFastScrollingRange || value < -mFastScrollingRange)
            && mScrollStart[dir] == 0) {
            // within auto scrolling area
            mScrollStart[dir] = ev.getEventTime();
        } else {
            // Outside fast scrolling area; reset duration
            mScrollStart[dir] = 0;
        }
        final float duration = (ev.getEventTime() - mScrollStart[dir])/mDurationToMax;
        final float interpolated;
        if (value < 0) {
            if (value < -mFastScrollingRange) {
                // Close to top; use duration!
                value += mEdgeInterpolator.getInterpolation(-duration);
            }
            interpolated = mEdgeInterpolator == null ? -1
                    : -mEdgeInterpolator.getInterpolation(-value);
        } else if (value > 0) {
            // Close to bottom; use duration
            if (value > mFastScrollingRange) {
                // Close to bottom; use duration!
                value += mEdgeInterpolator.getInterpolation(duration);
            }
            interpolated = mEdgeInterpolator == null ? 1
                    : mEdgeInterpolator.getInterpolation(value);
        } else {
            mScrollStart[dir] = 0;
            return 0;
        }

        return constrain(interpolated, -1, 1);
    }

    private static float constrain(float value, float min, float max) {
        if (value > max) {
            return max;
        } else if (value < min) {
            return min;
        } else {
            return value;
        }
    }

    /**
     * Stops auto-scrolling immediately, optionally reseting the auto-scrolling
     * delay.
     *
     * @param reset Whether to reset the auto-scrolling delay.
     */
    private void stop(boolean reset) {
        mActive = false;
        mSkipDelay = !reset;
        mTarget.removeCallbacks(this);
    }

    @Override
    public void run() {
        if (!mActive) {
            return;
        }

        if (mResetScroller) {
            mResetScroller = false;
            mScroller.start();
        }

        final View target = mTarget;
        final RampUpScroller scroller = mScroller;
        final float targetVelocityX = scroller.getTargetVelocityX();
        final float targetVelocityY = scroller.getTargetVelocityY();
        if ((targetVelocityY == 0 || !target.canScrollVertically(targetVelocityY > 0 ? 1 : -1)
                && (targetVelocityX == 0
                        || !target.canScrollHorizontally(targetVelocityX > 0 ? 1 : -1)))) {
            stop(false);
            return;
        }

        scroller.computeScrollDelta();

        final int deltaX = scroller.getDeltaX();
        final int deltaY = scroller.getDeltaY();

        if (target instanceof AbsListView) {
            final AbsListView list = (AbsListView) target;
            list.smoothScrollBy(deltaY, 0);
        } else {
            target.scrollBy(deltaX, deltaY);
        }

        target.postOnAnimation(this);
    }
}