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