Ripple.java revision 304624100b363137913d1d910690fc83872e41c3
1/*
2 * Copyright (C) 2013 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.graphics.drawable;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.graphics.Canvas;
24import android.graphics.CanvasProperty;
25import android.graphics.Paint;
26import android.graphics.Paint.Style;
27import android.graphics.Rect;
28import android.util.MathUtils;
29import android.view.HardwareCanvas;
30import android.view.RenderNodeAnimator;
31import android.view.animation.DecelerateInterpolator;
32import android.view.animation.LinearInterpolator;
33
34import java.util.ArrayList;
35
36/**
37 * Draws a Material ripple.
38 */
39class Ripple {
40    private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
41    private static final TimeInterpolator DECEL_INTERPOLATOR = new LogInterpolator();
42
43    private static final float GLOBAL_SPEED = 1.0f;
44    private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED;
45    private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED;
46    private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
47    private static final float WAVE_OUTER_OPACITY_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
48    private static final float WAVE_OUTER_OPACITY_VELOCITY_MIN = 1.5f * GLOBAL_SPEED;
49    private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f;
50    private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f;
51
52    private static final long RIPPLE_ENTER_DELAY = 80;
53
54    // Hardware animators.
55    private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>();
56    private final ArrayList<RenderNodeAnimator> mPendingAnimations = new ArrayList<>();
57
58    private final RippleDrawable mOwner;
59
60    /** Bounds used for computing max radius. */
61    private final Rect mBounds;
62
63    /** Full-opacity color for drawing this ripple. */
64    private int mColor;
65
66    /** Maximum ripple radius. */
67    private float mOuterRadius;
68
69    /** Screen density used to adjust pixel-based velocities. */
70    private float mDensity;
71
72    private float mStartingX;
73    private float mStartingY;
74    private float mClampedStartingX;
75    private float mClampedStartingY;
76
77    // Hardware rendering properties.
78    private CanvasProperty<Paint> mPropPaint;
79    private CanvasProperty<Float> mPropRadius;
80    private CanvasProperty<Float> mPropX;
81    private CanvasProperty<Float> mPropY;
82    private CanvasProperty<Paint> mPropOuterPaint;
83    private CanvasProperty<Float> mPropOuterRadius;
84    private CanvasProperty<Float> mPropOuterX;
85    private CanvasProperty<Float> mPropOuterY;
86
87    // Software animators.
88    private ObjectAnimator mAnimRadius;
89    private ObjectAnimator mAnimOpacity;
90    private ObjectAnimator mAnimOuterOpacity;
91    private ObjectAnimator mAnimX;
92    private ObjectAnimator mAnimY;
93
94    // Software rendering properties.
95    private float mOuterOpacity = 0;
96    private float mOpacity = 1;
97    private float mOuterX;
98    private float mOuterY;
99
100    // Values used to tween between the start and end positions.
101    private float mTweenRadius = 0;
102    private float mTweenX = 0;
103    private float mTweenY = 0;
104
105    /** Whether we should be drawing hardware animations. */
106    private boolean mHardwareAnimating;
107
108    /** Whether we can use hardware acceleration for the exit animation. */
109    private boolean mCanUseHardware;
110
111    /** Whether we have an explicit maximum radius. */
112    private boolean mHasMaxRadius;
113
114    /**
115     * Creates a new ripple.
116     */
117    public Ripple(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
118        mOwner = owner;
119        mBounds = bounds;
120
121        mStartingX = startingX;
122        mStartingY = startingY;
123    }
124
125    public void setup(int maxRadius, int color, float density) {
126        mColor = color | 0xFF000000;
127
128        if (maxRadius != RippleDrawable.RADIUS_AUTO) {
129            mHasMaxRadius = true;
130            mOuterRadius = maxRadius;
131        } else {
132            final float halfWidth = mBounds.width() / 2.0f;
133            final float halfHeight = mBounds.height() / 2.0f;
134            mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
135        }
136
137        mOuterX = 0;
138        mOuterY = 0;
139        mDensity = density;
140
141        clampStartingPosition();
142    }
143
144    private void clampStartingPosition() {
145        final float cX = mBounds.exactCenterX();
146        final float cY = mBounds.exactCenterY();
147        final float dX = mStartingX - cX;
148        final float dY = mStartingY - cY;
149        final float r = mOuterRadius;
150        if (dX * dX + dY * dY > r * r) {
151            // Point is outside the circle, clamp to the circumference.
152            final double angle = Math.atan2(dY, dX);
153            mClampedStartingX = cX + (float) (Math.cos(angle) * r);
154            mClampedStartingY = cY + (float) (Math.sin(angle) * r);
155        } else {
156            mClampedStartingX = mStartingX;
157            mClampedStartingY = mStartingY;
158        }
159    }
160
161    public void onHotspotBoundsChanged() {
162        if (!mHasMaxRadius) {
163            final float halfWidth = mBounds.width() / 2.0f;
164            final float halfHeight = mBounds.height() / 2.0f;
165            mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
166
167            clampStartingPosition();
168        }
169    }
170
171    public void setOpacity(float a) {
172        mOpacity = a;
173        invalidateSelf();
174    }
175
176    public float getOpacity() {
177        return mOpacity;
178    }
179
180    public void setOuterOpacity(float a) {
181        mOuterOpacity = a;
182        invalidateSelf();
183    }
184
185    public float getOuterOpacity() {
186        return mOuterOpacity;
187    }
188
189    public void setRadiusGravity(float r) {
190        mTweenRadius = r;
191        invalidateSelf();
192    }
193
194    public float getRadiusGravity() {
195        return mTweenRadius;
196    }
197
198    public void setXGravity(float x) {
199        mTweenX = x;
200        invalidateSelf();
201    }
202
203    public float getXGravity() {
204        return mTweenX;
205    }
206
207    public void setYGravity(float y) {
208        mTweenY = y;
209        invalidateSelf();
210    }
211
212    public float getYGravity() {
213        return mTweenY;
214    }
215
216    /**
217     * Draws the ripple centered at (0,0) using the specified paint.
218     */
219    public boolean draw(Canvas c, Paint p) {
220        final boolean canUseHardware = c.isHardwareAccelerated();
221        if (mCanUseHardware != canUseHardware && mCanUseHardware) {
222            // We've switched from hardware to non-hardware mode. Panic.
223            cancelHardwareAnimations();
224        }
225        mCanUseHardware = canUseHardware;
226
227        final boolean hasContent;
228        if (canUseHardware && mHardwareAnimating) {
229            hasContent = drawHardware((HardwareCanvas) c);
230        } else {
231            hasContent = drawSoftware(c, p);
232        }
233
234        return hasContent;
235    }
236
237    private boolean drawHardware(HardwareCanvas c) {
238        // If we have any pending hardware animations, cancel any running
239        // animations and start those now.
240        final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
241        final int N = pendingAnimations == null ? 0 : pendingAnimations.size();
242        if (N > 0) {
243            cancelHardwareAnimations();
244
245            for (int i = 0; i < N; i++) {
246                pendingAnimations.get(i).setTarget(c);
247                pendingAnimations.get(i).start();
248            }
249
250            mRunningAnimations.addAll(pendingAnimations);
251            pendingAnimations.clear();
252        }
253
254        c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
255        c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
256
257        return true;
258    }
259
260    private boolean drawSoftware(Canvas c, Paint p) {
261        boolean hasContent = false;
262
263        // Cache the paint alpha so we can restore it later.
264        final int paintAlpha = p.getAlpha();
265
266        final int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
267        if (outerAlpha > 0 && mOuterRadius > 0) {
268            p.setAlpha(outerAlpha);
269            p.setStyle(Style.FILL);
270            c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
271            hasContent = true;
272        }
273
274        final int alpha = (int) (paintAlpha * mOpacity + 0.5f);
275        final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
276        if (alpha > 0 && radius > 0) {
277            final float x = MathUtils.lerp(
278                    mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
279            final float y = MathUtils.lerp(
280                    mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
281            p.setAlpha(alpha);
282            p.setStyle(Style.FILL);
283            c.drawCircle(x, y, radius, p);
284            hasContent = true;
285        }
286
287        p.setAlpha(paintAlpha);
288
289        return hasContent;
290    }
291
292    /**
293     * Returns the maximum bounds of the ripple relative to the ripple center.
294     */
295    public void getBounds(Rect bounds) {
296        final int outerX = (int) mOuterX;
297        final int outerY = (int) mOuterY;
298        final int r = (int) mOuterRadius;
299        bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
300    }
301
302    /**
303     * Specifies the starting position relative to the drawable bounds. No-op if
304     * the ripple has already entered.
305     */
306    public void move(float x, float y) {
307        mStartingX = x;
308        mStartingY = y;
309
310        clampStartingPosition();
311    }
312
313    /**
314     * Starts the enter animation.
315     */
316    public void enter() {
317        final int radiusDuration = (int)
318                (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
319        final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY_MIN);
320
321        final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
322        radius.setAutoCancel(true);
323        radius.setDuration(radiusDuration);
324        radius.setInterpolator(LINEAR_INTERPOLATOR);
325        radius.setStartDelay(RIPPLE_ENTER_DELAY);
326
327        final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
328        cX.setAutoCancel(true);
329        cX.setDuration(radiusDuration);
330        cX.setInterpolator(LINEAR_INTERPOLATOR);
331        cX.setStartDelay(RIPPLE_ENTER_DELAY);
332
333        final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
334        cY.setAutoCancel(true);
335        cY.setDuration(radiusDuration);
336        cY.setInterpolator(LINEAR_INTERPOLATOR);
337        cY.setStartDelay(RIPPLE_ENTER_DELAY);
338
339        final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
340        outer.setAutoCancel(true);
341        outer.setDuration(outerDuration);
342        outer.setInterpolator(LINEAR_INTERPOLATOR);
343
344        mAnimRadius = radius;
345        mAnimOuterOpacity = outer;
346        mAnimX = cX;
347        mAnimY = cY;
348
349        // Enter animations always run on the UI thread, since it's unlikely
350        // that anything interesting is happening until the user lifts their
351        // finger.
352        radius.start();
353        outer.start();
354        cX.start();
355        cY.start();
356    }
357
358    /**
359     * Starts the exit animation.
360     */
361    public void exit() {
362        cancelSoftwareAnimations();
363        final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
364        final float remaining;
365        if (mAnimRadius != null && mAnimRadius.isRunning()) {
366            remaining = mOuterRadius - radius;
367        } else {
368            remaining = mOuterRadius;
369        }
370
371        final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
372                + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
373        final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
374
375        // Scale the outer max opacity and opacity velocity based
376        // on the size of the outer radius
377
378        float outerSizeInfluence = MathUtils.constrain(
379                (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
380                / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
381        float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_VELOCITY_MIN,
382                WAVE_OUTER_OPACITY_VELOCITY_MAX, outerSizeInfluence);
383
384        // Determine at what time the inner and outer opacity intersect.
385        // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
386        // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
387
388        final int outerInflection = Math.max(0, (int) (1000 * (mOpacity - mOuterOpacity)
389                / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
390        final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection
391                * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
392
393        if (mCanUseHardware) {
394            exitHardware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity);
395        } else {
396            exitSoftware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity);
397        }
398    }
399
400    private void exitHardware(int radiusDuration, int opacityDuration, int outerInflection,
401            int inflectionOpacity) {
402        mPendingAnimations.clear();
403
404        final float startX = MathUtils.lerp(
405                mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
406        final float startY = MathUtils.lerp(
407                mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
408        final Paint outerPaint = new Paint();
409        outerPaint.setAntiAlias(true);
410        outerPaint.setColor(mColor);
411        outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f));
412        outerPaint.setStyle(Style.FILL);
413        mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
414        mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
415        mPropOuterX = CanvasProperty.createFloat(mOuterX);
416        mPropOuterY = CanvasProperty.createFloat(mOuterY);
417
418        final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
419        final Paint paint = new Paint();
420        paint.setAntiAlias(true);
421        paint.setColor(mColor);
422        paint.setAlpha((int) (255 * mOpacity + 0.5f));
423        paint.setStyle(Style.FILL);
424        mPropPaint = CanvasProperty.createPaint(paint);
425        mPropRadius = CanvasProperty.createFloat(startRadius);
426        mPropX = CanvasProperty.createFloat(startX);
427        mPropY = CanvasProperty.createFloat(startY);
428
429        final RenderNodeAnimator radiusAnim = new RenderNodeAnimator(mPropRadius, mOuterRadius);
430        radiusAnim.setDuration(radiusDuration);
431        radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
432
433        final RenderNodeAnimator xAnim = new RenderNodeAnimator(mPropX, mOuterX);
434        xAnim.setDuration(radiusDuration);
435        xAnim.setInterpolator(DECEL_INTERPOLATOR);
436
437        final RenderNodeAnimator yAnim = new RenderNodeAnimator(mPropY, mOuterY);
438        yAnim.setDuration(radiusDuration);
439        yAnim.setInterpolator(DECEL_INTERPOLATOR);
440
441        final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPropPaint,
442                RenderNodeAnimator.PAINT_ALPHA, 0);
443        opacityAnim.setDuration(opacityDuration);
444        opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
445
446        final RenderNodeAnimator outerOpacityAnim;
447        if (outerInflection > 0) {
448            // Outer opacity continues to increase for a bit.
449            outerOpacityAnim = new RenderNodeAnimator(
450                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
451            outerOpacityAnim.setDuration(outerInflection);
452            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
453
454            // Chain the outer opacity exit animation.
455            final int outerDuration = opacityDuration - outerInflection;
456            if (outerDuration > 0) {
457                final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
458                        mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
459                outerFadeOutAnim.setDuration(outerDuration);
460                outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
461                outerFadeOutAnim.setStartDelay(outerInflection);
462                outerFadeOutAnim.setStartValue(inflectionOpacity);
463                outerFadeOutAnim.addListener(mAnimationListener);
464
465                mPendingAnimations.add(outerFadeOutAnim);
466            } else {
467                outerOpacityAnim.addListener(mAnimationListener);
468            }
469        } else {
470            outerOpacityAnim = new RenderNodeAnimator(
471                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
472            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
473            outerOpacityAnim.setDuration(opacityDuration);
474            outerOpacityAnim.addListener(mAnimationListener);
475        }
476
477        mPendingAnimations.add(radiusAnim);
478        mPendingAnimations.add(opacityAnim);
479        mPendingAnimations.add(outerOpacityAnim);
480        mPendingAnimations.add(xAnim);
481        mPendingAnimations.add(yAnim);
482
483        mHardwareAnimating = true;
484
485        invalidateSelf();
486    }
487
488    private void exitSoftware(int radiusDuration, int opacityDuration, int outerInflection,
489            int inflectionOpacity) {
490        final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
491        radiusAnim.setAutoCancel(true);
492        radiusAnim.setDuration(radiusDuration);
493        radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
494
495        final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
496        xAnim.setAutoCancel(true);
497        xAnim.setDuration(radiusDuration);
498        xAnim.setInterpolator(DECEL_INTERPOLATOR);
499
500        final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
501        yAnim.setAutoCancel(true);
502        yAnim.setDuration(radiusDuration);
503        yAnim.setInterpolator(DECEL_INTERPOLATOR);
504
505        final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0);
506        opacityAnim.setAutoCancel(true);
507        opacityAnim.setDuration(opacityDuration);
508        opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
509
510        final ObjectAnimator outerOpacityAnim;
511        if (outerInflection > 0) {
512            // Outer opacity continues to increase for a bit.
513            outerOpacityAnim = ObjectAnimator.ofFloat(this,
514                    "outerOpacity", inflectionOpacity / 255.0f);
515            outerOpacityAnim.setAutoCancel(true);
516            outerOpacityAnim.setDuration(outerInflection);
517            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
518
519            // Chain the outer opacity exit animation.
520            final int outerDuration = opacityDuration - outerInflection;
521            if (outerDuration > 0) {
522                outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
523                    @Override
524                    public void onAnimationEnd(Animator animation) {
525                        final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(Ripple.this,
526                                "outerOpacity", 0);
527                        outerFadeOutAnim.setAutoCancel(true);
528                        outerFadeOutAnim.setDuration(outerDuration);
529                        outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
530                        outerFadeOutAnim.addListener(mAnimationListener);
531
532                        mAnimOuterOpacity = outerFadeOutAnim;
533
534                        outerFadeOutAnim.start();
535                    }
536
537                    @Override
538                    public void onAnimationCancel(Animator animation) {
539                        animation.removeListener(this);
540                    }
541                });
542            } else {
543                outerOpacityAnim.addListener(mAnimationListener);
544            }
545        } else {
546            outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
547            outerOpacityAnim.setAutoCancel(true);
548            outerOpacityAnim.setDuration(opacityDuration);
549            outerOpacityAnim.addListener(mAnimationListener);
550        }
551
552        mAnimRadius = radiusAnim;
553        mAnimOpacity = opacityAnim;
554        mAnimOuterOpacity = outerOpacityAnim;
555        mAnimX = opacityAnim;
556        mAnimY = opacityAnim;
557
558        radiusAnim.start();
559        opacityAnim.start();
560        outerOpacityAnim.start();
561        xAnim.start();
562        yAnim.start();
563    }
564
565    /**
566     * Cancel all animations.
567     */
568    public void cancel() {
569        cancelSoftwareAnimations();
570        cancelHardwareAnimations();
571    }
572
573    private void cancelSoftwareAnimations() {
574        if (mAnimRadius != null) {
575            mAnimRadius.cancel();
576        }
577
578        if (mAnimOpacity != null) {
579            mAnimOpacity.cancel();
580        }
581
582        if (mAnimOuterOpacity != null) {
583            mAnimOuterOpacity.cancel();
584        }
585
586        if (mAnimX != null) {
587            mAnimX.cancel();
588        }
589
590        if (mAnimY != null) {
591            mAnimY.cancel();
592        }
593    }
594
595    /**
596     * Cancels any running hardware animations.
597     */
598    private void cancelHardwareAnimations() {
599        final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
600        final int N = runningAnimations == null ? 0 : runningAnimations.size();
601        for (int i = 0; i < N; i++) {
602            runningAnimations.get(i).cancel();
603        }
604
605        runningAnimations.clear();
606    }
607
608    private void removeSelf() {
609        // The owner will invalidate itself.
610        mOwner.removeRipple(this);
611    }
612
613    private void invalidateSelf() {
614        mOwner.invalidateSelf();
615    }
616
617    private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
618        @Override
619        public void onAnimationEnd(Animator animation) {
620            removeSelf();
621        }
622    };
623
624    /**
625    * Interpolator with a smooth log deceleration
626    */
627    private static final class LogInterpolator implements TimeInterpolator {
628        @Override
629        public float getInterpolation(float input) {
630            return 1 - (float) Math.pow(400, -input * 1.4);
631        }
632    }
633}
634