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