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