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