OverScroller.java revision 0ee0a2ea57197cb2f03905454098d9a7a309f77b
1/*
2 * Copyright (C) 2006 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
17package android.widget;
18
19import android.content.Context;
20import android.view.animation.AnimationUtils;
21import android.view.animation.Interpolator;
22
23/**
24 * This class encapsulates scrolling with the ability to overshoot the bounds
25 * of a scrolling operation. This class attempts to be a drop-in replacement
26 * for {@link android.widget.Scroller} in most cases.
27 *
28 * @hide Pending API approval
29 */
30public class OverScroller extends Scroller {
31
32    // Identical to mScrollers, but casted to MagneticOverScroller.
33    private MagneticOverScroller mOverScrollerX;
34    private MagneticOverScroller mOverScrollerY;
35
36    /**
37     * Creates an OverScroller with a viscous fluid scroll interpolator.
38     * @param context
39     */
40    public OverScroller(Context context) {
41        this(context, null);
42    }
43
44    /**
45     * Creates an OverScroller with default edge bounce coefficients.
46     * @param context The context of this application.
47     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
48     * be used.
49     */
50    public OverScroller(Context context, Interpolator interpolator) {
51        this(context, interpolator, MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT,
52                MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT);
53    }
54
55    /**
56     * Creates an OverScroller.
57     * @param context The context of this application.
58     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
59     * be used.
60     * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
61     * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
62     * means no bounce.
63     * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction.
64     */
65    public OverScroller(Context context, Interpolator interpolator,
66            float bounceCoefficientX, float bounceCoefficientY) {
67        super(context, interpolator);
68        mOverScrollerX.setBounceCoefficient(bounceCoefficientX);
69        mOverScrollerY.setBounceCoefficient(bounceCoefficientY);
70    }
71
72    @Override
73    void instantiateScrollers() {
74        mScrollerX = mOverScrollerX = new MagneticOverScroller();
75        mScrollerY = mOverScrollerY = new MagneticOverScroller();
76    }
77
78    /**
79     * Call this when you want to 'spring back' into a valid coordinate range.
80     *
81     * @param startX Starting X coordinate
82     * @param startY Starting Y coordinate
83     * @param minX Minimum valid X value
84     * @param maxX Maximum valid X value
85     * @param minY Minimum valid Y value
86     * @param maxY Minimum valid Y value
87     * @return true if a springback was initiated, false if startX and startY were
88     *          already within the valid range.
89     */
90    public boolean springback(int startX, int startY, int minX, int maxX, int minY, int maxY) {
91        mMode = FLING_MODE;
92
93        // Make sure both methods are called.
94        final boolean spingbackX = mOverScrollerX.springback(startX, minX, maxX);
95        final boolean spingbackY = mOverScrollerY.springback(startY, minY, maxY);
96        return spingbackX || spingbackY;
97    }
98
99    @Override
100    public void fling(int startX, int startY, int velocityX, int velocityY,
101            int minX, int maxX, int minY, int maxY) {
102        fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
103    }
104
105    /**
106     * Start scrolling based on a fling gesture. The distance traveled will
107     * depend on the initial velocity of the fling.
108     *
109     * @param startX Starting point of the scroll (X)
110     * @param startY Starting point of the scroll (Y)
111     * @param velocityX Initial velocity of the fling (X) measured in pixels per
112     *            second.
113     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
114     *            second
115     * @param minX Minimum X value. The scroller will not scroll past this point
116     *            unless overX > 0. If overfling is allowed, it will use minX as
117     *            a springback boundary.
118     * @param maxX Maximum X value. The scroller will not scroll past this point
119     *            unless overX > 0. If overfling is allowed, it will use maxX as
120     *            a springback boundary.
121     * @param minY Minimum Y value. The scroller will not scroll past this point
122     *            unless overY > 0. If overfling is allowed, it will use minY as
123     *            a springback boundary.
124     * @param maxY Maximum Y value. The scroller will not scroll past this point
125     *            unless overY > 0. If overfling is allowed, it will use maxY as
126     *            a springback boundary.
127     * @param overX Overfling range. If > 0, horizontal overfling in either
128     *            direction will be possible.
129     * @param overY Overfling range. If > 0, vertical overfling in either
130     *            direction will be possible.
131     */
132    public void fling(int startX, int startY, int velocityX, int velocityY,
133            int minX, int maxX, int minY, int maxY, int overX, int overY) {
134        mMode = FLING_MODE;
135        mOverScrollerX.fling(startX, velocityX, minX, maxX, overX);
136        mOverScrollerY.fling(startY, velocityY, minY, maxY, overY);
137    }
138
139    void notifyHorizontalBoundaryReached(int startX, int finalX) {
140        mOverScrollerX.springback(startX, finalX, finalX);
141    }
142
143    void notifyVerticalBoundaryReached(int startY, int finalY) {
144        mOverScrollerY.springback(startY, finalY, finalY);
145    }
146
147    void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
148        mOverScrollerX.notifyEdgeReached(startX, finalX, overX);
149    }
150
151    void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
152        mOverScrollerY.notifyEdgeReached(startY, finalY, overY);
153    }
154
155    /**
156     * Returns whether the current Scroller is currently returning to a valid position.
157     * Valid bounds were provided by the
158     * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
159     *
160     * One should check this value before calling
161     * {@link startScroll(int, int, int, int)} as the interpolation currently in progress to restore
162     * a valid position will then be stopped. The caller has to take into account the fact that the
163     * started scroll will start from an overscrolled position.
164     *
165     * @return true when the current position is overscrolled and interpolated back to a valid value.
166     */
167    public boolean isOverscrolled() {
168        return ((!mOverScrollerX.mFinished &&
169                mOverScrollerX.mState != MagneticOverScroller.TO_EDGE) ||
170                (!mOverScrollerY.mFinished &&
171                        mOverScrollerY.mState != MagneticOverScroller.TO_EDGE));
172    }
173
174    static class MagneticOverScroller extends Scroller.MagneticScroller {
175        private static final int TO_EDGE = 0;
176        private static final int TO_BOUNDARY = 1;
177        private static final int TO_BOUNCE = 2;
178
179        private int mState = TO_EDGE;
180
181        // The allowed overshot distance before boundary is reached.
182        private int mOver;
183
184        // Duration in milliseconds to go back from edge to edge. Springback is half of it.
185        private static final int OVERSCROLL_SPRINGBACK_DURATION = 200;
186
187        // Oscillation period
188        private static final float TIME_COEF =
189            1000.0f * (float) Math.PI / OVERSCROLL_SPRINGBACK_DURATION;
190
191        // If the velocity is smaller than this value, no bounce is triggered
192        // when the edge limits are reached (would result in a zero pixels
193        // displacement anyway).
194        private static final float MINIMUM_VELOCITY_FOR_BOUNCE = 140.0f;
195
196        // Proportion of the velocity that is preserved when the edge is reached.
197        private static final float DEFAULT_BOUNCE_COEFFICIENT = 0.16f;
198
199        private float mBounceCoefficient = DEFAULT_BOUNCE_COEFFICIENT;
200
201        void setBounceCoefficient(float coefficient) {
202            mBounceCoefficient = coefficient;
203        }
204
205        boolean springback(int start, int min, int max) {
206            mFinished = true;
207
208            mStart = start;
209            mVelocity = 0;
210
211            mStartTime = AnimationUtils.currentAnimationTimeMillis();
212            mDuration = 0;
213
214            if (start < min) {
215                startSpringback(start, min, false);
216            } else if (start > max) {
217                startSpringback(start, max, true);
218            }
219
220            return !mFinished;
221        }
222
223        private void startSpringback(int start, int end, boolean positive) {
224            mFinished = false;
225            mState = TO_BOUNCE;
226            mStart = mFinal = end;
227            mDuration = OVERSCROLL_SPRINGBACK_DURATION;
228            mStartTime -= OVERSCROLL_SPRINGBACK_DURATION / 2;
229            mVelocity = (int) (Math.abs(end - start) * TIME_COEF * (positive ? 1.0 : -1.0f));
230        }
231
232        void fling(int start, int velocity, int min, int max, int over) {
233            mState = TO_EDGE;
234            mOver = over;
235
236            super.fling(start, velocity, min, max);
237
238            if (start > max) {
239                if (start >= max + over) {
240                    springback(max + over, min, max);
241                } else {
242                    if (velocity <= 0) {
243                        springback(start, min, max);
244                    } else {
245                        long time = AnimationUtils.currentAnimationTimeMillis();
246                        final double durationSinceEdge =
247                            Math.atan((start-max) * TIME_COEF / velocity) / TIME_COEF;
248                        mStartTime = (int) (time - 1000.0f * durationSinceEdge);
249
250                        // Simulate a bounce that started from edge
251                        mStart = max;
252
253                        mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF));
254
255                        onEdgeReached();
256                    }
257                }
258            } else {
259                if (start < min) {
260                    if (start <= min - over) {
261                        springback(min - over, min, max);
262                    } else {
263                        if (velocity >= 0) {
264                            springback(start, min, max);
265                        } else {
266                            long time = AnimationUtils.currentAnimationTimeMillis();
267                            final double durationSinceEdge =
268                                Math.atan((start-min) * TIME_COEF / velocity) / TIME_COEF;
269                            mStartTime = (int) (time - 1000.0f * durationSinceEdge);
270
271                            // Simulate a bounce that started from edge
272                            mStart = min;
273
274                            mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF));
275
276                            onEdgeReached();
277                        }
278
279                    }
280                }
281            }
282        }
283
284        void notifyEdgeReached(int start, int end, int over) {
285            mDeceleration = getDeceleration(mVelocity);
286
287            // Local time, used to compute edge crossing time.
288            float timeCurrent = mCurrVelocity / mDeceleration;
289            final int distance = end - start;
290            float timeEdge = -(float) Math.sqrt((2.0f * distance / mDeceleration)
291                    + (timeCurrent * timeCurrent));
292
293            mVelocity = (int) (mDeceleration * timeEdge);
294
295            // Simulate a symmetric bounce that started from edge
296            mStart = end;
297
298            mOver = over;
299
300            long time = AnimationUtils.currentAnimationTimeMillis();
301            mStartTime = (int) (time - 1000.0f * (timeCurrent - timeEdge));
302
303            onEdgeReached();
304        }
305
306        private void onEdgeReached() {
307            // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
308            final float distance = mVelocity / TIME_COEF;
309
310            if (Math.abs(distance) < mOver) {
311                // Spring force will bring us back to final position
312                mState = TO_BOUNCE;
313                mFinal = mStart;
314                mDuration = OVERSCROLL_SPRINGBACK_DURATION;
315            } else {
316                // Velocity is too high, we will hit the boundary limit
317                mState = TO_BOUNDARY;
318                int over = mVelocity > 0 ? mOver : -mOver;
319                mFinal = mStart + over;
320                mDuration = (int) (1000.0f * Math.asin(over / distance) / TIME_COEF);
321            }
322        }
323
324        @Override
325        boolean continueWhenFinished() {
326            switch (mState) {
327                case TO_EDGE:
328                    // Duration from start to null velocity
329                    int duration = (int) (-1000.0f * mVelocity / mDeceleration);
330                    if (mDuration < duration) {
331                        // If the animation was clamped, we reached the edge
332                        mStart = mFinal;
333                        // Speed when edge was reached
334                        mVelocity = (int) (mVelocity + mDeceleration * mDuration / 1000.0f);
335                        mStartTime += mDuration;
336                        onEdgeReached();
337                    } else {
338                        // Normal stop, no need to continue
339                        return false;
340                    }
341                    break;
342                case TO_BOUNDARY:
343                    mStartTime += mDuration;
344                    startSpringback(mFinal, mFinal - (mVelocity > 0 ? mOver:-mOver), mVelocity > 0);
345                    break;
346                case TO_BOUNCE:
347                    //mVelocity = (int) (mVelocity * BOUNCE_COEFFICIENT);
348                    mVelocity = (int) (mVelocity * mBounceCoefficient);
349                    if (Math.abs(mVelocity) < MINIMUM_VELOCITY_FOR_BOUNCE) {
350                        return false;
351                    }
352                    mStartTime += mDuration;
353                    break;
354            }
355
356            update();
357            return true;
358        }
359
360        /*
361         * Update the current position and velocity for current time. Returns
362         * true if update has been done and false if animation duration has been
363         * reached.
364         */
365        @Override
366        boolean update() {
367            final long time = AnimationUtils.currentAnimationTimeMillis();
368            final long duration = time - mStartTime;
369
370            if (duration > mDuration) {
371                return false;
372            }
373
374            double distance;
375            final float t = duration / 1000.0f;
376            if (mState == TO_EDGE) {
377                mCurrVelocity = mVelocity + mDeceleration * t;
378                distance = mVelocity * t + mDeceleration * t * t / 2.0f;
379            } else {
380                final float d = t * TIME_COEF;
381                mCurrVelocity = mVelocity * (float)Math.cos(d);
382                distance = mVelocity / TIME_COEF * Math.sin(d);
383            }
384
385            mCurrentPosition = mStart + (int) distance;
386            return true;
387        }
388    }
389}
390