blob: 3bd0a78c47fe546a6c3656d2596a9c4f37914bee [file] [log] [blame]
Adam Cohenf9618852013-11-08 06:45:03 -08001/*
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 com.android.launcher3;
18
19import android.animation.TimeInterpolator;
20import android.content.Context;
21import android.hardware.SensorManager;
22import android.os.Build;
23import android.util.FloatMath;
24import android.view.ViewConfiguration;
25import android.view.animation.AnimationUtils;
26import android.view.animation.Interpolator;
27
28/**
29 * This class differs from the framework {@link android.widget.Scroller} in that
30 * you can modify the Interpolator post-construction.
31 */
32public class LauncherScroller {
33 private int mMode;
34
35 private int mStartX;
36 private int mStartY;
37 private int mFinalX;
38 private int mFinalY;
39
40 private int mMinX;
41 private int mMaxX;
42 private int mMinY;
43 private int mMaxY;
44
45 private int mCurrX;
46 private int mCurrY;
47 private long mStartTime;
48 private int mDuration;
49 private float mDurationReciprocal;
50 private float mDeltaX;
51 private float mDeltaY;
52 private boolean mFinished;
53 private TimeInterpolator mInterpolator;
54 private boolean mFlywheel;
55
56 private float mVelocity;
57 private float mCurrVelocity;
58 private int mDistance;
59
60 private float mFlingFriction = ViewConfiguration.getScrollFriction();
61
62 private static final int DEFAULT_DURATION = 250;
63 private static final int SCROLL_MODE = 0;
64 private static final int FLING_MODE = 1;
65
66 private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
67 private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
68 private static final float START_TENSION = 0.5f;
69 private static final float END_TENSION = 1.0f;
70 private static final float P1 = START_TENSION * INFLEXION;
71 private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
72
73 private static final int NB_SAMPLES = 100;
74 private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
75 private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
76
77 private float mDeceleration;
78 private final float mPpi;
79
80 // A context-specific coefficient adjusted to physical values.
81 private float mPhysicalCoeff;
82
83 static {
84 float x_min = 0.0f;
85 float y_min = 0.0f;
86 for (int i = 0; i < NB_SAMPLES; i++) {
87 final float alpha = (float) i / NB_SAMPLES;
88
89 float x_max = 1.0f;
90 float x, tx, coef;
91 while (true) {
92 x = x_min + (x_max - x_min) / 2.0f;
93 coef = 3.0f * x * (1.0f - x);
94 tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
95 if (Math.abs(tx - alpha) < 1E-5) break;
96 if (tx > alpha) x_max = x;
97 else x_min = x;
98 }
99 SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
100
101 float y_max = 1.0f;
102 float y, dy;
103 while (true) {
104 y = y_min + (y_max - y_min) / 2.0f;
105 coef = 3.0f * y * (1.0f - y);
106 dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
107 if (Math.abs(dy - alpha) < 1E-5) break;
108 if (dy > alpha) y_max = y;
109 else y_min = y;
110 }
111 SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
112 }
113 SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
114
115 // This controls the viscous fluid effect (how much of it)
116 sViscousFluidScale = 8.0f;
117 // must be set to 1.0 (used in viscousFluid())
118 sViscousFluidNormalize = 1.0f;
119 sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
120
121 }
122
123 private static float sViscousFluidScale;
124 private static float sViscousFluidNormalize;
125
126 public void setInterpolator(TimeInterpolator interpolator) {
127 mInterpolator = interpolator;
128 }
129
130 /**
131 * Create a Scroller with the default duration and interpolator.
132 */
133 public LauncherScroller(Context context) {
134 this(context, null);
135 }
136
137 /**
138 * Create a Scroller with the specified interpolator. If the interpolator is
139 * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
140 * be in effect for apps targeting Honeycomb or newer.
141 */
142 public LauncherScroller(Context context, Interpolator interpolator) {
143 this(context, interpolator,
144 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
145 }
146
147 /**
148 * Create a Scroller with the specified interpolator. If the interpolator is
149 * null, the default (viscous) interpolator will be used. Specify whether or
150 * not to support progressive "flywheel" behavior in flinging.
151 */
152 public LauncherScroller(Context context, Interpolator interpolator, boolean flywheel) {
153 mFinished = true;
154 mInterpolator = interpolator;
155 mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
156 mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
157 mFlywheel = flywheel;
158
159 mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
160 }
161
162 /**
163 * The amount of friction applied to flings. The default value
164 * is {@link ViewConfiguration#getScrollFriction}.
165 *
166 * @param friction A scalar dimension-less value representing the coefficient of
167 * friction.
168 */
169 public final void setFriction(float friction) {
170 mDeceleration = computeDeceleration(friction);
171 mFlingFriction = friction;
172 }
173
174 private float computeDeceleration(float friction) {
175 return SensorManager.GRAVITY_EARTH // g (m/s^2)
176 * 39.37f // inch/meter
177 * mPpi // pixels per inch
178 * friction;
179 }
180
181 /**
182 *
183 * Returns whether the scroller has finished scrolling.
184 *
185 * @return True if the scroller has finished scrolling, false otherwise.
186 */
187 public final boolean isFinished() {
188 return mFinished;
189 }
190
191 /**
192 * Force the finished field to a particular value.
193 *
194 * @param finished The new finished value.
195 */
196 public final void forceFinished(boolean finished) {
197 mFinished = finished;
198 }
199
200 /**
201 * Returns how long the scroll event will take, in milliseconds.
202 *
203 * @return The duration of the scroll in milliseconds.
204 */
205 public final int getDuration() {
206 return mDuration;
207 }
208
209 /**
210 * Returns the current X offset in the scroll.
211 *
212 * @return The new X offset as an absolute distance from the origin.
213 */
214 public final int getCurrX() {
215 return mCurrX;
216 }
217
218 /**
219 * Returns the current Y offset in the scroll.
220 *
221 * @return The new Y offset as an absolute distance from the origin.
222 */
223 public final int getCurrY() {
224 return mCurrY;
225 }
226
227 /**
228 * Returns the current velocity.
229 *
230 * @return The original velocity less the deceleration. Result may be
231 * negative.
232 */
233 public float getCurrVelocity() {
234 return mMode == FLING_MODE ?
235 mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f;
236 }
237
238 /**
239 * Returns the start X offset in the scroll.
240 *
241 * @return The start X offset as an absolute distance from the origin.
242 */
243 public final int getStartX() {
244 return mStartX;
245 }
246
247 /**
248 * Returns the start Y offset in the scroll.
249 *
250 * @return The start Y offset as an absolute distance from the origin.
251 */
252 public final int getStartY() {
253 return mStartY;
254 }
255
256 /**
257 * Returns where the scroll will end. Valid only for "fling" scrolls.
258 *
259 * @return The final X offset as an absolute distance from the origin.
260 */
261 public final int getFinalX() {
262 return mFinalX;
263 }
264
265 /**
266 * Returns where the scroll will end. Valid only for "fling" scrolls.
267 *
268 * @return The final Y offset as an absolute distance from the origin.
269 */
270 public final int getFinalY() {
271 return mFinalY;
272 }
273
274 /**
275 * Call this when you want to know the new location. If it returns true,
276 * the animation is not yet finished.
277 */
278 public boolean computeScrollOffset() {
279 if (mFinished) {
280 return false;
281 }
282
283 int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
284
285 if (timePassed < mDuration) {
286 switch (mMode) {
287 case SCROLL_MODE:
288 float x = timePassed * mDurationReciprocal;
289
290 if (mInterpolator == null)
291 x = viscousFluid(x);
292 else
293 x = mInterpolator.getInterpolation(x);
294
295 mCurrX = mStartX + Math.round(x * mDeltaX);
296 mCurrY = mStartY + Math.round(x * mDeltaY);
297 break;
298 case FLING_MODE:
299 final float t = (float) timePassed / mDuration;
300 final int index = (int) (NB_SAMPLES * t);
301 float distanceCoef = 1.f;
302 float velocityCoef = 0.f;
303 if (index < NB_SAMPLES) {
304 final float t_inf = (float) index / NB_SAMPLES;
305 final float t_sup = (float) (index + 1) / NB_SAMPLES;
306 final float d_inf = SPLINE_POSITION[index];
307 final float d_sup = SPLINE_POSITION[index + 1];
308 velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
309 distanceCoef = d_inf + (t - t_inf) * velocityCoef;
310 }
311
312 mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
313
314 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
315 // Pin to mMinX <= mCurrX <= mMaxX
316 mCurrX = Math.min(mCurrX, mMaxX);
317 mCurrX = Math.max(mCurrX, mMinX);
318
319 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
320 // Pin to mMinY <= mCurrY <= mMaxY
321 mCurrY = Math.min(mCurrY, mMaxY);
322 mCurrY = Math.max(mCurrY, mMinY);
323
324 if (mCurrX == mFinalX && mCurrY == mFinalY) {
325 mFinished = true;
326 }
327
328 break;
329 }
330 }
331 else {
332 mCurrX = mFinalX;
333 mCurrY = mFinalY;
334 mFinished = true;
335 }
336 return true;
337 }
338
339 /**
340 * Start scrolling by providing a starting point and the distance to travel.
341 * The scroll will use the default value of 250 milliseconds for the
342 * duration.
343 *
344 * @param startX Starting horizontal scroll offset in pixels. Positive
345 * numbers will scroll the content to the left.
346 * @param startY Starting vertical scroll offset in pixels. Positive numbers
347 * will scroll the content up.
348 * @param dx Horizontal distance to travel. Positive numbers will scroll the
349 * content to the left.
350 * @param dy Vertical distance to travel. Positive numbers will scroll the
351 * content up.
352 */
353 public void startScroll(int startX, int startY, int dx, int dy) {
354 startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
355 }
356
357 /**
358 * Start scrolling by providing a starting point, the distance to travel,
359 * and the duration of the scroll.
360 *
361 * @param startX Starting horizontal scroll offset in pixels. Positive
362 * numbers will scroll the content to the left.
363 * @param startY Starting vertical scroll offset in pixels. Positive numbers
364 * will scroll the content up.
365 * @param dx Horizontal distance to travel. Positive numbers will scroll the
366 * content to the left.
367 * @param dy Vertical distance to travel. Positive numbers will scroll the
368 * content up.
369 * @param duration Duration of the scroll in milliseconds.
370 */
371 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
372 mMode = SCROLL_MODE;
373 mFinished = false;
374 mDuration = duration;
375 mStartTime = AnimationUtils.currentAnimationTimeMillis();
376 mStartX = startX;
377 mStartY = startY;
378 mFinalX = startX + dx;
379 mFinalY = startY + dy;
380 mDeltaX = dx;
381 mDeltaY = dy;
382 mDurationReciprocal = 1.0f / (float) mDuration;
383 }
384
385 /**
386 * Start scrolling based on a fling gesture. The distance travelled will
387 * depend on the initial velocity of the fling.
388 *
389 * @param startX Starting point of the scroll (X)
390 * @param startY Starting point of the scroll (Y)
391 * @param velocityX Initial velocity of the fling (X) measured in pixels per
392 * second.
393 * @param velocityY Initial velocity of the fling (Y) measured in pixels per
394 * second
395 * @param minX Minimum X value. The scroller will not scroll past this
396 * point.
397 * @param maxX Maximum X value. The scroller will not scroll past this
398 * point.
399 * @param minY Minimum Y value. The scroller will not scroll past this
400 * point.
401 * @param maxY Maximum Y value. The scroller will not scroll past this
402 * point.
403 */
404 public void fling(int startX, int startY, int velocityX, int velocityY,
405 int minX, int maxX, int minY, int maxY) {
406 // Continue a scroll or fling in progress
407 if (mFlywheel && !mFinished) {
408 float oldVel = getCurrVelocity();
409
410 float dx = (float) (mFinalX - mStartX);
411 float dy = (float) (mFinalY - mStartY);
412 float hyp = FloatMath.sqrt(dx * dx + dy * dy);
413
414 float ndx = dx / hyp;
415 float ndy = dy / hyp;
416
417 float oldVelocityX = ndx * oldVel;
418 float oldVelocityY = ndy * oldVel;
419 if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
420 Math.signum(velocityY) == Math.signum(oldVelocityY)) {
421 velocityX += oldVelocityX;
422 velocityY += oldVelocityY;
423 }
424 }
425
426 mMode = FLING_MODE;
427 mFinished = false;
428
429 float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
430
431 mVelocity = velocity;
432 mDuration = getSplineFlingDuration(velocity);
433 mStartTime = AnimationUtils.currentAnimationTimeMillis();
434 mStartX = startX;
435 mStartY = startY;
436
437 float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
438 float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
439
440 double totalDistance = getSplineFlingDistance(velocity);
441 mDistance = (int) (totalDistance * Math.signum(velocity));
442
443 mMinX = minX;
444 mMaxX = maxX;
445 mMinY = minY;
446 mMaxY = maxY;
447
448 mFinalX = startX + (int) Math.round(totalDistance * coeffX);
449 // Pin to mMinX <= mFinalX <= mMaxX
450 mFinalX = Math.min(mFinalX, mMaxX);
451 mFinalX = Math.max(mFinalX, mMinX);
452
453 mFinalY = startY + (int) Math.round(totalDistance * coeffY);
454 // Pin to mMinY <= mFinalY <= mMaxY
455 mFinalY = Math.min(mFinalY, mMaxY);
456 mFinalY = Math.max(mFinalY, mMinY);
457 }
458
459 private double getSplineDeceleration(float velocity) {
460 return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
461 }
462
463 private int getSplineFlingDuration(float velocity) {
464 final double l = getSplineDeceleration(velocity);
465 final double decelMinusOne = DECELERATION_RATE - 1.0;
466 return (int) (1000.0 * Math.exp(l / decelMinusOne));
467 }
468
469 private double getSplineFlingDistance(float velocity) {
470 final double l = getSplineDeceleration(velocity);
471 final double decelMinusOne = DECELERATION_RATE - 1.0;
472 return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
473 }
474
475 static float viscousFluid(float x)
476 {
477 x *= sViscousFluidScale;
478 if (x < 1.0f) {
479 x -= (1.0f - (float)Math.exp(-x));
480 } else {
481 float start = 0.36787944117f; // 1/e == exp(-1)
482 x = 1.0f - (float)Math.exp(1.0f - x);
483 x = start + x * (1.0f - start);
484 }
485 x *= sViscousFluidNormalize;
486 return x;
487 }
488
489 /**
490 * Stops the animation. Contrary to {@link #forceFinished(boolean)},
491 * aborting the animating cause the scroller to move to the final x and y
492 * position
493 *
494 * @see #forceFinished(boolean)
495 */
496 public void abortAnimation() {
497 mCurrX = mFinalX;
498 mCurrY = mFinalY;
499 mFinished = true;
500 }
501
502 /**
503 * Extend the scroll animation. This allows a running animation to scroll
504 * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
505 *
506 * @param extend Additional time to scroll in milliseconds.
507 * @see #setFinalX(int)
508 * @see #setFinalY(int)
509 */
510 public void extendDuration(int extend) {
511 int passed = timePassed();
512 mDuration = passed + extend;
513 mDurationReciprocal = 1.0f / mDuration;
514 mFinished = false;
515 }
516
517 /**
518 * Returns the time elapsed since the beginning of the scrolling.
519 *
520 * @return The elapsed time in milliseconds.
521 */
522 public int timePassed() {
523 return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
524 }
525
526 /**
527 * Sets the final position (X) for this scroller.
528 *
529 * @param newX The new X offset as an absolute distance from the origin.
530 * @see #extendDuration(int)
531 * @see #setFinalY(int)
532 */
533 public void setFinalX(int newX) {
534 mFinalX = newX;
535 mDeltaX = mFinalX - mStartX;
536 mFinished = false;
537 }
538
539 /**
540 * Sets the final position (Y) for this scroller.
541 *
542 * @param newY The new Y offset as an absolute distance from the origin.
543 * @see #extendDuration(int)
544 * @see #setFinalX(int)
545 */
546 public void setFinalY(int newY) {
547 mFinalY = newY;
548 mDeltaY = mFinalY - mStartY;
549 mFinished = false;
550 }
551
552 /**
553 * @hide
554 */
555 public boolean isScrollingInDirection(float xvel, float yvel) {
556 return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) &&
557 Math.signum(yvel) == Math.signum(mFinalY - mStartY);
558 }
559}