RippleBackground.java revision fdbb98e56d4668c7bfa8de59c3c438c0cb69a535
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.LinearInterpolator; 32 33import java.util.ArrayList; 34 35/** 36 * Draws a Material ripple. 37 */ 38class RippleBackground { 39 private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 40 private static final TimeInterpolator DECEL_INTERPOLATOR = new LogInterpolator(); 41 42 private static final float GLOBAL_SPEED = 1.0f; 43 private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED; 44 private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED; 45 private static final float WAVE_OUTER_OPACITY_VELOCITY_MAX = 4.5f * GLOBAL_SPEED; 46 private static final float WAVE_OUTER_OPACITY_VELOCITY_MIN = 1.5f * GLOBAL_SPEED; 47 private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f; 48 private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f; 49 50 private static final long RIPPLE_ENTER_DELAY = 80; 51 52 // Hardware animators. 53 private final ArrayList<RenderNodeAnimator> mRunningAnimations = 54 new ArrayList<RenderNodeAnimator>(); 55 private final ArrayList<RenderNodeAnimator> mPendingAnimations = 56 new ArrayList<RenderNodeAnimator>(); 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> mPropOuterPaint; 79 private CanvasProperty<Float> mPropOuterRadius; 80 private CanvasProperty<Float> mPropOuterX; 81 private CanvasProperty<Float> mPropOuterY; 82 83 // Software animators. 84 private ObjectAnimator mAnimOuterOpacity; 85 private ObjectAnimator mAnimX; 86 private ObjectAnimator mAnimY; 87 88 // Temporary paint used for creating canvas properties. 89 private Paint mTempPaint; 90 91 // Software rendering properties. 92 private float mOuterOpacity = 0; 93 private float mOuterX; 94 private float mOuterY; 95 96 // Values used to tween between the start and end positions. 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 RippleBackground(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 mColor = 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 @SuppressWarnings("unused") 170 public void setOuterOpacity(float a) { 171 mOuterOpacity = a; 172 invalidateSelf(); 173 } 174 175 @SuppressWarnings("unused") 176 public float getOuterOpacity() { 177 return mOuterOpacity; 178 } 179 180 @SuppressWarnings("unused") 181 public void setXGravity(float x) { 182 mTweenX = x; 183 invalidateSelf(); 184 } 185 186 @SuppressWarnings("unused") 187 public float getXGravity() { 188 return mTweenX; 189 } 190 191 @SuppressWarnings("unused") 192 public void setYGravity(float y) { 193 mTweenY = y; 194 invalidateSelf(); 195 } 196 197 @SuppressWarnings("unused") 198 public float getYGravity() { 199 return mTweenY; 200 } 201 202 /** 203 * Draws the ripple centered at (0,0) using the specified paint. 204 */ 205 public boolean draw(Canvas c, Paint p) { 206 final boolean canUseHardware = c.isHardwareAccelerated(); 207 if (mCanUseHardware != canUseHardware && mCanUseHardware) { 208 // We've switched from hardware to non-hardware mode. Panic. 209 cancelHardwareAnimations(true); 210 } 211 mCanUseHardware = canUseHardware; 212 213 final boolean hasContent; 214 if (canUseHardware && mHardwareAnimating) { 215 hasContent = drawHardware((HardwareCanvas) c); 216 } else { 217 hasContent = drawSoftware(c, p); 218 } 219 220 return hasContent; 221 } 222 223 private boolean drawHardware(HardwareCanvas c) { 224 // If we have any pending hardware animations, cancel any running 225 // animations and start those now. 226 final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations; 227 final int N = pendingAnimations.size(); 228 if (N > 0) { 229 cancelHardwareAnimations(false); 230 231 for (int i = 0; i < N; i++) { 232 pendingAnimations.get(i).setTarget(c); 233 pendingAnimations.get(i).start(); 234 } 235 236 mRunningAnimations.addAll(pendingAnimations); 237 pendingAnimations.clear(); 238 } 239 240 c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint); 241 242 return true; 243 } 244 245 private boolean drawSoftware(Canvas c, Paint p) { 246 boolean hasContent = false; 247 248 // Cache the paint alpha so we can restore it later. 249 final int paintAlpha = p.getAlpha(); 250 251 final int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5f); 252 if (outerAlpha > 0 && mOuterRadius > 0) { 253 p.setAlpha(outerAlpha); 254 p.setStyle(Style.FILL); 255 c.drawCircle(mOuterX, mOuterY, mOuterRadius, p); 256 hasContent = true; 257 } 258 259 p.setAlpha(paintAlpha); 260 261 return hasContent; 262 } 263 264 /** 265 * Returns the maximum bounds of the ripple relative to the ripple center. 266 */ 267 public void getBounds(Rect bounds) { 268 final int outerX = (int) mOuterX; 269 final int outerY = (int) mOuterY; 270 final int r = (int) mOuterRadius + 1; 271 bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); 272 } 273 274 /** 275 * Specifies the starting position relative to the drawable bounds. No-op if 276 * the ripple has already entered. 277 */ 278 public void move(float x, float y) { 279 mStartingX = x; 280 mStartingY = y; 281 282 clampStartingPosition(); 283 } 284 285 /** 286 * Starts the enter animation. 287 */ 288 public void enter() { 289 cancel(); 290 291 final int radiusDuration = (int) 292 (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5); 293 final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY_MIN); 294 295 final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1); 296 cX.setAutoCancel(true); 297 cX.setDuration(radiusDuration); 298 cX.setInterpolator(LINEAR_INTERPOLATOR); 299 cX.setStartDelay(RIPPLE_ENTER_DELAY); 300 301 final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1); 302 cY.setAutoCancel(true); 303 cY.setDuration(radiusDuration); 304 cY.setInterpolator(LINEAR_INTERPOLATOR); 305 cY.setStartDelay(RIPPLE_ENTER_DELAY); 306 307 final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1); 308 outer.setAutoCancel(true); 309 outer.setDuration(outerDuration); 310 outer.setInterpolator(LINEAR_INTERPOLATOR); 311 312 mAnimOuterOpacity = outer; 313 mAnimX = cX; 314 mAnimY = cY; 315 316 // Enter animations always run on the UI thread, since it's unlikely 317 // that anything interesting is happening until the user lifts their 318 // finger. 319 outer.start(); 320 cX.start(); 321 cY.start(); 322 } 323 324 /** 325 * Starts the exit animation. 326 */ 327 public void exit() { 328 cancel(); 329 330 // Scale the outer max opacity and opacity velocity based 331 // on the size of the outer radius. 332 final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); 333 final float outerSizeInfluence = MathUtils.constrain( 334 (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity) 335 / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1); 336 final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_VELOCITY_MIN, 337 WAVE_OUTER_OPACITY_VELOCITY_MAX, outerSizeInfluence); 338 339 // Determine at what time the inner and outer opacity intersect. 340 // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000 341 // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000 342 final int outerInflection = Math.max(0, (int) (1000 * (1 - mOuterOpacity) 343 / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f)); 344 final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection 345 * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f); 346 347 if (mCanUseHardware) { 348 exitHardware(opacityDuration, outerInflection, inflectionOpacity); 349 } else { 350 exitSoftware(opacityDuration, outerInflection, inflectionOpacity); 351 } 352 } 353 354 private void exitHardware(int opacityDuration, int outerInflection, int inflectionOpacity) { 355 mPendingAnimations.clear(); 356 357 // TODO: Adjust background by starting position. 358 final float startX = MathUtils.lerp( 359 mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); 360 final float startY = MathUtils.lerp( 361 mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); 362 363 final Paint outerPaint = getTempPaint(); 364 outerPaint.setAntiAlias(true); 365 outerPaint.setColor(mColor); 366 outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f)); 367 outerPaint.setStyle(Style.FILL); 368 mPropOuterPaint = CanvasProperty.createPaint(outerPaint); 369 mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius); 370 mPropOuterX = CanvasProperty.createFloat(mOuterX); 371 mPropOuterY = CanvasProperty.createFloat(mOuterY); 372 373 final RenderNodeAnimator outerOpacityAnim; 374 if (outerInflection > 0) { 375 // Outer opacity continues to increase for a bit. 376 outerOpacityAnim = new RenderNodeAnimator( 377 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity); 378 outerOpacityAnim.setDuration(outerInflection); 379 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 380 381 // Chain the outer opacity exit animation. 382 final int outerDuration = opacityDuration - outerInflection; 383 if (outerDuration > 0) { 384 final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator( 385 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); 386 outerFadeOutAnim.setDuration(outerDuration); 387 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); 388 outerFadeOutAnim.setStartDelay(outerInflection); 389 outerFadeOutAnim.setStartValue(inflectionOpacity); 390 outerFadeOutAnim.addListener(mAnimationListener); 391 392 mPendingAnimations.add(outerFadeOutAnim); 393 } else { 394 outerOpacityAnim.addListener(mAnimationListener); 395 } 396 } else { 397 outerOpacityAnim = new RenderNodeAnimator( 398 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); 399 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 400 outerOpacityAnim.setDuration(opacityDuration); 401 outerOpacityAnim.addListener(mAnimationListener); 402 } 403 404 mPendingAnimations.add(outerOpacityAnim); 405 406 mHardwareAnimating = true; 407 408 invalidateSelf(); 409 } 410 411 /** 412 * Jump all animations to their end state. The caller is responsible for 413 * removing the ripple from the list of animating ripples. 414 */ 415 public void jump() { 416 mCanceled = true; 417 endSoftwareAnimations(); 418 endHardwareAnimations(); 419 mCanceled = false; 420 } 421 422 private void endSoftwareAnimations() { 423 if (mAnimOuterOpacity != null) { 424 mAnimOuterOpacity.end(); 425 } 426 427 if (mAnimX != null) { 428 mAnimX.end(); 429 } 430 431 if (mAnimY != null) { 432 mAnimY.end(); 433 } 434 } 435 436 private void endHardwareAnimations() { 437 final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; 438 final int N = runningAnimations.size(); 439 for (int i = 0; i < N; i++) { 440 runningAnimations.get(i).end(); 441 } 442 runningAnimations.clear(); 443 444 // Abort any pending animations. Since we always have a completion 445 // listener on a pending animation, we also need to remove ourselves. 446 if (!mPendingAnimations.isEmpty()) { 447 mPendingAnimations.clear(); 448 removeSelf(); 449 } 450 451 mHardwareAnimating = false; 452 } 453 454 private Paint getTempPaint() { 455 if (mTempPaint == null) { 456 mTempPaint = new Paint(); 457 } 458 return mTempPaint; 459 } 460 461 private void exitSoftware(int opacityDuration, int outerInflection, int inflectionOpacity) { 462 final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1); 463 xAnim.setAutoCancel(true); 464 xAnim.setDuration(opacityDuration); 465 xAnim.setInterpolator(DECEL_INTERPOLATOR); 466 467 final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1); 468 yAnim.setAutoCancel(true); 469 yAnim.setDuration(opacityDuration); 470 yAnim.setInterpolator(DECEL_INTERPOLATOR); 471 472 final ObjectAnimator outerOpacityAnim; 473 if (outerInflection > 0) { 474 // Outer opacity continues to increase for a bit. 475 outerOpacityAnim = ObjectAnimator.ofFloat(this, 476 "outerOpacity", inflectionOpacity / 255.0f); 477 outerOpacityAnim.setAutoCancel(true); 478 outerOpacityAnim.setDuration(outerInflection); 479 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 480 481 // Chain the outer opacity exit animation. 482 final int outerDuration = opacityDuration - outerInflection; 483 if (outerDuration > 0) { 484 outerOpacityAnim.addListener(new AnimatorListenerAdapter() { 485 @Override 486 public void onAnimationEnd(Animator animation) { 487 final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat( 488 RippleBackground.this, "outerOpacity", 0); 489 outerFadeOutAnim.setAutoCancel(true); 490 outerFadeOutAnim.setDuration(outerDuration); 491 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); 492 outerFadeOutAnim.addListener(mAnimationListener); 493 494 mAnimOuterOpacity = outerFadeOutAnim; 495 496 outerFadeOutAnim.start(); 497 } 498 499 @Override 500 public void onAnimationCancel(Animator animation) { 501 animation.removeListener(this); 502 } 503 }); 504 } else { 505 outerOpacityAnim.addListener(mAnimationListener); 506 } 507 } else { 508 outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0); 509 outerOpacityAnim.setAutoCancel(true); 510 outerOpacityAnim.setDuration(opacityDuration); 511 outerOpacityAnim.addListener(mAnimationListener); 512 } 513 514 mAnimOuterOpacity = outerOpacityAnim; 515 mAnimX = xAnim; 516 mAnimY = yAnim; 517 518 outerOpacityAnim.start(); 519 xAnim.start(); 520 yAnim.start(); 521 } 522 523 /** 524 * Cancel all animations. The caller is responsible for removing 525 * the ripple from the list of animating ripples. 526 */ 527 public void cancel() { 528 mCanceled = true; 529 cancelSoftwareAnimations(); 530 cancelHardwareAnimations(true); 531 mCanceled = false; 532 } 533 534 private void cancelSoftwareAnimations() { 535 if (mAnimOuterOpacity != null) { 536 mAnimOuterOpacity.cancel(); 537 } 538 539 if (mAnimX != null) { 540 mAnimX.cancel(); 541 } 542 543 if (mAnimY != null) { 544 mAnimY.cancel(); 545 } 546 } 547 548 /** 549 * Cancels any running hardware animations. 550 */ 551 private void cancelHardwareAnimations(boolean cancelPending) { 552 final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; 553 final int N = runningAnimations.size(); 554 for (int i = 0; i < N; i++) { 555 runningAnimations.get(i).cancel(); 556 } 557 558 runningAnimations.clear(); 559 560 if (cancelPending && !mPendingAnimations.isEmpty()) { 561 mPendingAnimations.clear(); 562 } 563 564 mHardwareAnimating = false; 565 } 566 567 private void removeSelf() { 568 // The owner will invalidate itself. 569 if (!mCanceled) { 570 mOwner.removeBackground(this); 571 } 572 } 573 574 private void invalidateSelf() { 575 mOwner.invalidateSelf(); 576 } 577 578 private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { 579 @Override 580 public void onAnimationEnd(Animator animation) { 581 removeSelf(); 582 } 583 }; 584 585 /** 586 * Interpolator with a smooth log deceleration 587 */ 588 private static final class LogInterpolator implements TimeInterpolator { 589 @Override 590 public float getInterpolation(float input) { 591 return 1 - (float) Math.pow(400, -input * 1.4); 592 } 593 } 594} 595