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