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