2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.widget;
19 import android.content.Context;
20 import android.hardware.SensorManager;
21 import android.util.FloatMath;
22 import android.view.ViewConfiguration;
23 import android.view.animation.AnimationUtils;
24 import android.view.animation.Interpolator;
27 * This class encapsulates scrolling with the ability to overshoot the bounds
28 * of a scrolling operation. This class is a drop-in replacement for
29 * {@link android.widget.Scroller} in most cases.
31 public class OverScroller {
34 private MagneticOverScroller mScrollerX;
35 private MagneticOverScroller mScrollerY;
37 private final Interpolator mInterpolator;
39 private static final int DEFAULT_DURATION = 250;
40 private static final int SCROLL_MODE = 0;
41 private static final int FLING_MODE = 1;
44 * Creates an OverScroller with a viscous fluid scroll interpolator.
47 public OverScroller(Context context) {
52 * Creates an OverScroller with default edge bounce coefficients.
53 * @param context The context of this application.
54 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
57 public OverScroller(Context context, Interpolator interpolator) {
58 this(context, interpolator, MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT,
59 MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT);
63 * Creates an OverScroller.
64 * @param context The context of this application.
65 * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
67 * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
68 * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
70 * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction.
72 public OverScroller(Context context, Interpolator interpolator,
73 float bounceCoefficientX, float bounceCoefficientY) {
74 mInterpolator = interpolator;
75 mScrollerX = new MagneticOverScroller();
76 mScrollerY = new MagneticOverScroller();
77 MagneticOverScroller.initializeFromContext(context);
79 mScrollerX.setBounceCoefficient(bounceCoefficientX);
80 mScrollerY.setBounceCoefficient(bounceCoefficientY);
85 * Returns whether the scroller has finished scrolling.
87 * @return True if the scroller has finished scrolling, false otherwise.
89 public final boolean isFinished() {
90 return mScrollerX.mFinished && mScrollerY.mFinished;
94 * Force the finished field to a particular value. Contrary to
95 * {@link #abortAnimation()}, forcing the animation to finished
96 * does NOT cause the scroller to move to the final x and y
99 * @param finished The new finished value.
101 public final void forceFinished(boolean finished) {
102 mScrollerX.mFinished = mScrollerY.mFinished = finished;
106 * Returns the current X offset in the scroll.
108 * @return The new X offset as an absolute distance from the origin.
110 public final int getCurrX() {
111 return mScrollerX.mCurrentPosition;
115 * Returns the current Y offset in the scroll.
117 * @return The new Y offset as an absolute distance from the origin.
119 public final int getCurrY() {
120 return mScrollerY.mCurrentPosition;
125 * Returns the current velocity.
127 * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
129 public float getCurrVelocity() {
130 float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity;
131 squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity;
132 return FloatMath.sqrt(squaredNorm);
136 * Returns the start X offset in the scroll.
138 * @return The start X offset as an absolute distance from the origin.
140 public final int getStartX() {
141 return mScrollerX.mStart;
145 * Returns the start Y offset in the scroll.
147 * @return The start Y offset as an absolute distance from the origin.
149 public final int getStartY() {
150 return mScrollerY.mStart;
154 * Returns where the scroll will end. Valid only for "fling" scrolls.
156 * @return The final X offset as an absolute distance from the origin.
158 public final int getFinalX() {
159 return mScrollerX.mFinal;
163 * Returns where the scroll will end. Valid only for "fling" scrolls.
165 * @return The final Y offset as an absolute distance from the origin.
167 public final int getFinalY() {
168 return mScrollerY.mFinal;
172 * Returns how long the scroll event will take, in milliseconds.
174 * @return The duration of the scroll in milliseconds.
176 * @hide Pending removal once nothing depends on it
177 * @deprecated OverScrollers don't necessarily have a fixed duration.
178 * This function will lie to the best of its ability.
180 public final int getDuration() {
181 return Math.max(mScrollerX.mDuration, mScrollerY.mDuration);
185 * Extend the scroll animation. This allows a running animation to scroll
186 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
188 * @param extend Additional time to scroll in milliseconds.
189 * @see #setFinalX(int)
190 * @see #setFinalY(int)
192 * @hide Pending removal once nothing depends on it
193 * @deprecated OverScrollers don't necessarily have a fixed duration.
194 * Instead of setting a new final position and extending
195 * the duration of an existing scroll, use startScroll
196 * to begin a new animation.
198 public void extendDuration(int extend) {
199 mScrollerX.extendDuration(extend);
200 mScrollerY.extendDuration(extend);
204 * Sets the final position (X) for this scroller.
206 * @param newX The new X offset as an absolute distance from the origin.
207 * @see #extendDuration(int)
208 * @see #setFinalY(int)
210 * @hide Pending removal once nothing depends on it
211 * @deprecated OverScroller's final position may change during an animation.
212 * Instead of setting a new final position and extending
213 * the duration of an existing scroll, use startScroll
214 * to begin a new animation.
216 public void setFinalX(int newX) {
217 mScrollerX.setFinalPosition(newX);
221 * Sets the final position (Y) for this scroller.
223 * @param newY The new Y offset as an absolute distance from the origin.
224 * @see #extendDuration(int)
225 * @see #setFinalX(int)
227 * @hide Pending removal once nothing depends on it
228 * @deprecated OverScroller's final position may change during an animation.
229 * Instead of setting a new final position and extending
230 * the duration of an existing scroll, use startScroll
231 * to begin a new animation.
233 public void setFinalY(int newY) {
234 mScrollerY.setFinalPosition(newY);
238 * Call this when you want to know the new location. If it returns true, the
239 * animation is not yet finished.
241 public boolean computeScrollOffset() {
248 long time = AnimationUtils.currentAnimationTimeMillis();
249 // Any scroller can be used for time, since they were started
250 // together in scroll mode. We use X here.
251 final long elapsedTime = time - mScrollerX.mStartTime;
253 final int duration = mScrollerX.mDuration;
254 if (elapsedTime < duration) {
255 float q = (float) (elapsedTime) / duration;
257 if (mInterpolator == null)
258 q = Scroller.viscousFluid(q);
260 q = mInterpolator.getInterpolation(q);
262 mScrollerX.updateScroll(q);
263 mScrollerY.updateScroll(q);
270 if (!mScrollerX.mFinished) {
271 if (!mScrollerX.update()) {
272 if (!mScrollerX.continueWhenFinished()) {
278 if (!mScrollerY.mFinished) {
279 if (!mScrollerY.update()) {
280 if (!mScrollerY.continueWhenFinished()) {
293 * Start scrolling by providing a starting point and the distance to travel.
294 * The scroll will use the default value of 250 milliseconds for the
297 * @param startX Starting horizontal scroll offset in pixels. Positive
298 * numbers will scroll the content to the left.
299 * @param startY Starting vertical scroll offset in pixels. Positive numbers
300 * will scroll the content up.
301 * @param dx Horizontal distance to travel. Positive numbers will scroll the
302 * content to the left.
303 * @param dy Vertical distance to travel. Positive numbers will scroll the
306 public void startScroll(int startX, int startY, int dx, int dy) {
307 startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
311 * Start scrolling by providing a starting point and the distance to travel.
313 * @param startX Starting horizontal scroll offset in pixels. Positive
314 * numbers will scroll the content to the left.
315 * @param startY Starting vertical scroll offset in pixels. Positive numbers
316 * will scroll the content up.
317 * @param dx Horizontal distance to travel. Positive numbers will scroll the
318 * content to the left.
319 * @param dy Vertical distance to travel. Positive numbers will scroll the
321 * @param duration Duration of the scroll in milliseconds.
323 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
325 mScrollerX.startScroll(startX, dx, duration);
326 mScrollerY.startScroll(startY, dy, duration);
330 * Call this when you want to 'spring back' into a valid coordinate range.
332 * @param startX Starting X coordinate
333 * @param startY Starting Y coordinate
334 * @param minX Minimum valid X value
335 * @param maxX Maximum valid X value
336 * @param minY Minimum valid Y value
337 * @param maxY Minimum valid Y value
338 * @return true if a springback was initiated, false if startX and startY were
339 * already within the valid range.
341 public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
344 // Make sure both methods are called.
345 final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
346 final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
347 return spingbackX || spingbackY;
350 public void fling(int startX, int startY, int velocityX, int velocityY,
351 int minX, int maxX, int minY, int maxY) {
352 fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
356 * Start scrolling based on a fling gesture. The distance traveled will
357 * depend on the initial velocity of the fling.
359 * @param startX Starting point of the scroll (X)
360 * @param startY Starting point of the scroll (Y)
361 * @param velocityX Initial velocity of the fling (X) measured in pixels per
363 * @param velocityY Initial velocity of the fling (Y) measured in pixels per
365 * @param minX Minimum X value. The scroller will not scroll past this point
366 * unless overX > 0. If overfling is allowed, it will use minX as
367 * a springback boundary.
368 * @param maxX Maximum X value. The scroller will not scroll past this point
369 * unless overX > 0. If overfling is allowed, it will use maxX as
370 * a springback boundary.
371 * @param minY Minimum Y value. The scroller will not scroll past this point
372 * unless overY > 0. If overfling is allowed, it will use minY as
373 * a springback boundary.
374 * @param maxY Maximum Y value. The scroller will not scroll past this point
375 * unless overY > 0. If overfling is allowed, it will use maxY as
376 * a springback boundary.
377 * @param overX Overfling range. If > 0, horizontal overfling in either
378 * direction will be possible.
379 * @param overY Overfling range. If > 0, vertical overfling in either
380 * direction will be possible.
382 public void fling(int startX, int startY, int velocityX, int velocityY,
383 int minX, int maxX, int minY, int maxY, int overX, int overY) {
385 mScrollerX.fling(startX, velocityX, minX, maxX, overX);
386 mScrollerY.fling(startY, velocityY, minY, maxY, overY);
390 * Notify the scroller that we've reached a horizontal boundary.
391 * Normally the information to handle this will already be known
392 * when the animation is started, such as in a call to one of the
393 * fling functions. However there are cases where this cannot be known
394 * in advance. This function will transition the current motion and
395 * animate from startX to finalX as appropriate.
397 * @param startX Starting/current X position
398 * @param finalX Desired final X position
399 * @param overX Magnitude of overscroll allowed. This should be the maximum
400 * desired distance from finalX. Absolute value - must be positive.
402 public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
403 mScrollerX.notifyEdgeReached(startX, finalX, overX);
407 * Notify the scroller that we've reached a vertical boundary.
408 * Normally the information to handle this will already be known
409 * when the animation is started, such as in a call to one of the
410 * fling functions. However there are cases where this cannot be known
411 * in advance. This function will animate a parabolic motion from
414 * @param startY Starting/current Y position
415 * @param finalY Desired final Y position
416 * @param overY Magnitude of overscroll allowed. This should be the maximum
417 * desired distance from finalY.
419 public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
420 mScrollerY.notifyEdgeReached(startY, finalY, overY);
424 * Returns whether the current Scroller is currently returning to a valid position.
425 * Valid bounds were provided by the
426 * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
428 * One should check this value before calling
429 * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress
430 * to restore a valid position will then be stopped. The caller has to take into account
431 * the fact that the started scroll will start from an overscrolled position.
433 * @return true when the current position is overscrolled and in the process of
434 * interpolating back to a valid value.
436 public boolean isOverScrolled() {
437 return ((!mScrollerX.mFinished &&
438 mScrollerX.mState != MagneticOverScroller.TO_EDGE) ||
439 (!mScrollerY.mFinished &&
440 mScrollerY.mState != MagneticOverScroller.TO_EDGE));
444 * Stops the animation. Contrary to {@link #forceFinished(boolean)},
445 * aborting the animating causes the scroller to move to the final x and y
448 * @see #forceFinished(boolean)
450 public void abortAnimation() {
456 * Returns the time elapsed since the beginning of the scrolling.
458 * @return The elapsed time in milliseconds.
462 public int timePassed() {
463 final long time = AnimationUtils.currentAnimationTimeMillis();
464 final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime);
465 return (int) (time - startTime);
468 static class MagneticOverScroller {
473 int mCurrentPosition;
484 // Constant current deceleration
487 // Animation starting time, in system milliseconds
490 // Animation duration, in milliseconds
493 // Whether the animation is currently in progress
496 // Constant gravity value, used to scale deceleration
497 static float GRAVITY;
499 static void initializeFromContext(Context context) {
500 final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
501 GRAVITY = SensorManager.GRAVITY_EARTH // g (m/s^2)
502 * 39.37f // inch/meter
503 * ppi // pixels per inch
504 * ViewConfiguration.getScrollFriction();
507 private static final int TO_EDGE = 0;
508 private static final int TO_BOUNDARY = 1;
509 private static final int TO_BOUNCE = 2;
511 private int mState = TO_EDGE;
513 // The allowed overshot distance before boundary is reached.
516 // Duration in milliseconds to go back from edge to edge. Springback is half of it.
517 private static final int OVERSCROLL_SPRINGBACK_DURATION = 200;
519 // Oscillation period
520 private static final float TIME_COEF =
521 1000.0f * (float) Math.PI / OVERSCROLL_SPRINGBACK_DURATION;
523 // If the velocity is smaller than this value, no bounce is triggered
524 // when the edge limits are reached (would result in a zero pixels
525 // displacement anyway).
526 private static final float MINIMUM_VELOCITY_FOR_BOUNCE = Float.MAX_VALUE;//140.0f;
528 // Proportion of the velocity that is preserved when the edge is reached.
529 private static final float DEFAULT_BOUNCE_COEFFICIENT = 0.16f;
531 private float mBounceCoefficient = DEFAULT_BOUNCE_COEFFICIENT;
533 MagneticOverScroller() {
537 void updateScroll(float q) {
538 mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
542 * Get a signed deceleration that will reduce the velocity.
544 static float getDeceleration(int velocity) {
545 return velocity > 0 ? -GRAVITY : GRAVITY;
549 * Returns the time (in milliseconds) it will take to go from start to end.
551 static int computeDuration(int start, int end, float initialVelocity, float deceleration) {
552 final int distance = start - end;
553 final float discriminant = initialVelocity * initialVelocity - 2.0f * deceleration
555 if (discriminant >= 0.0f) {
556 float delta = (float) Math.sqrt(discriminant);
557 if (deceleration < 0.0f) {
560 return (int) (1000.0f * (-initialVelocity - delta) / deceleration);
563 // End position can not be reached
567 void startScroll(int start, int distance, int duration) {
571 mFinal = start + distance;
573 mStartTime = AnimationUtils.currentAnimationTimeMillis();
574 mDuration = duration;
577 mDeceleration = 0.0f;
581 void fling(int start, int velocity, int min, int max) {
585 mStartTime = AnimationUtils.currentAnimationTimeMillis();
587 mVelocity = velocity;
589 mDeceleration = getDeceleration(velocity);
591 // A start from an invalid position immediately brings back to a valid position
604 // Duration are expressed in milliseconds
605 mDuration = (int) (-1000.0f * velocity / mDeceleration);
607 mFinal = start - Math.round((velocity * velocity) / (2.0f * mDeceleration));
609 // Clamp to a valid final position
612 mDuration = computeDuration(mStart, min, mVelocity, mDeceleration);
617 mDuration = computeDuration(mStart, max, mVelocity, mDeceleration);
622 mCurrentPosition = mFinal;
623 // Not reset since WebView relies on this value for fast fling.
624 // mCurrVelocity = 0.0f;
628 void setFinalPosition(int position) {
633 void extendDuration(int extend) {
634 final long time = AnimationUtils.currentAnimationTimeMillis();
635 final int elapsedTime = (int) (time - mStartTime);
636 mDuration = elapsedTime + extend;
640 void setBounceCoefficient(float coefficient) {
641 mBounceCoefficient = coefficient;
644 boolean springback(int start, int min, int max) {
650 mStartTime = AnimationUtils.currentAnimationTimeMillis();
654 startSpringback(start, min, false);
655 } else if (start > max) {
656 startSpringback(start, max, true);
662 private void startSpringback(int start, int end, boolean positive) {
665 mStart = mFinal = end;
666 mDuration = OVERSCROLL_SPRINGBACK_DURATION;
667 mStartTime -= OVERSCROLL_SPRINGBACK_DURATION / 2;
668 mVelocity = (int) (Math.abs(end - start) * TIME_COEF * (positive ? 1.0 : -1.0f));
671 void fling(int start, int velocity, int min, int max, int over) {
678 mStartTime = AnimationUtils.currentAnimationTimeMillis();
680 mVelocity = velocity;
682 mDeceleration = getDeceleration(velocity);
684 // Duration are expressed in milliseconds
685 mDuration = (int) (-1000.0f * velocity / mDeceleration);
687 mFinal = start - Math.round((velocity * velocity) / (2.0f * mDeceleration));
689 // Clamp to a valid final position
692 mDuration = computeDuration(mStart, min, mVelocity, mDeceleration);
697 mDuration = computeDuration(mStart, max, mVelocity, mDeceleration);
701 if (start >= max + over) {
702 springback(max + over, min, max);
705 springback(start, min, max);
707 long time = AnimationUtils.currentAnimationTimeMillis();
708 final double durationSinceEdge =
709 Math.atan((start-max) * TIME_COEF / velocity) / TIME_COEF;
710 mStartTime = (int) (time - 1000.0f * durationSinceEdge);
712 // Simulate a bounce that started from edge
715 mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF));
722 if (start <= min - over) {
723 springback(min - over, min, max);
726 springback(start, min, max);
728 long time = AnimationUtils.currentAnimationTimeMillis();
729 final double durationSinceEdge =
730 Math.atan((start-min) * TIME_COEF / velocity) / TIME_COEF;
731 mStartTime = (int) (time - 1000.0f * durationSinceEdge);
733 // Simulate a bounce that started from edge
736 mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF));
746 void notifyEdgeReached(int start, int end, int over) {
747 mDeceleration = getDeceleration(mVelocity);
749 // Local time, used to compute edge crossing time.
750 float timeCurrent = mCurrVelocity / mDeceleration;
751 final int distance = end - start;
752 float timeEdge = -(float) Math.sqrt((2.0f * distance / mDeceleration)
753 + (timeCurrent * timeCurrent));
755 mVelocity = (int) (mDeceleration * timeEdge);
757 // Simulate a symmetric bounce that started from edge
762 long time = AnimationUtils.currentAnimationTimeMillis();
763 mStartTime = (int) (time - 1000.0f * (timeCurrent - timeEdge));
768 private void onEdgeReached() {
769 // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
770 final float distance = mVelocity / TIME_COEF;
772 if (Math.abs(distance) < mOver) {
773 // Spring force will bring us back to final position
776 mDuration = OVERSCROLL_SPRINGBACK_DURATION;
778 // Velocity is too high, we will hit the boundary limit
779 mState = TO_BOUNDARY;
780 int over = mVelocity > 0 ? mOver : -mOver;
781 mFinal = mStart + over;
782 mDuration = (int) (1000.0f * Math.asin(over / distance) / TIME_COEF);
786 boolean continueWhenFinished() {
789 // Duration from start to null velocity
790 int duration = (int) (-1000.0f * mVelocity / mDeceleration);
791 if (mDuration < duration) {
792 // If the animation was clamped, we reached the edge
794 // Speed when edge was reached
795 mVelocity = (int) (mVelocity + mDeceleration * mDuration / 1000.0f);
796 mStartTime += mDuration;
799 // Normal stop, no need to continue
804 mStartTime += mDuration;
805 startSpringback(mFinal, mFinal - (mVelocity > 0 ? mOver:-mOver), mVelocity > 0);
808 //mVelocity = (int) (mVelocity * BOUNCE_COEFFICIENT);
809 mVelocity = (int) (mVelocity * mBounceCoefficient);
810 if (Math.abs(mVelocity) < MINIMUM_VELOCITY_FOR_BOUNCE) {
813 mStartTime += mDuration;
822 * Update the current position and velocity for current time. Returns
823 * true if update has been done and false if animation duration has been
827 final long time = AnimationUtils.currentAnimationTimeMillis();
828 final long duration = time - mStartTime;
830 if (duration > mDuration) {
835 final float t = duration / 1000.0f;
836 if (mState == TO_EDGE) {
837 mCurrVelocity = mVelocity + mDeceleration * t;
838 distance = mVelocity * t + mDeceleration * t * t / 2.0f;
840 final float d = t * TIME_COEF;
841 mCurrVelocity = mVelocity * (float)Math.cos(d);
842 distance = mVelocity / TIME_COEF * Math.sin(d);
845 mCurrentPosition = mStart + (int) distance;