RippleBackground.java revision 6ce6d70f9c78f0197f1369246bf55a5f6b8d7ba4
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 /** 110 * Creates a new ripple. 111 */ 112 public RippleBackground(RippleDrawable owner, Rect bounds, float startingX, float startingY) { 113 mOwner = owner; 114 mBounds = bounds; 115 116 mStartingX = startingX; 117 mStartingY = startingY; 118 } 119 120 public void setup(int maxRadius, int color, float density) { 121 mColor = color | 0xFF000000; 122 123 if (maxRadius != RippleDrawable.RADIUS_AUTO) { 124 mHasMaxRadius = true; 125 mOuterRadius = maxRadius; 126 } else { 127 final float halfWidth = mBounds.width() / 2.0f; 128 final float halfHeight = mBounds.height() / 2.0f; 129 mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); 130 } 131 132 mOuterX = 0; 133 mOuterY = 0; 134 mDensity = density; 135 136 clampStartingPosition(); 137 } 138 139 private void clampStartingPosition() { 140 final float cX = mBounds.exactCenterX(); 141 final float cY = mBounds.exactCenterY(); 142 final float dX = mStartingX - cX; 143 final float dY = mStartingY - cY; 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 = cX + (float) (Math.cos(angle) * r); 149 mClampedStartingY = cY + (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 @SuppressWarnings("unused") 167 public void setOuterOpacity(float a) { 168 mOuterOpacity = a; 169 invalidateSelf(); 170 } 171 172 @SuppressWarnings("unused") 173 public float getOuterOpacity() { 174 return mOuterOpacity; 175 } 176 177 @SuppressWarnings("unused") 178 public void setXGravity(float x) { 179 mTweenX = x; 180 invalidateSelf(); 181 } 182 183 @SuppressWarnings("unused") 184 public float getXGravity() { 185 return mTweenX; 186 } 187 188 @SuppressWarnings("unused") 189 public void setYGravity(float y) { 190 mTweenY = y; 191 invalidateSelf(); 192 } 193 194 @SuppressWarnings("unused") 195 public float getYGravity() { 196 return mTweenY; 197 } 198 199 /** 200 * Draws the ripple centered at (0,0) using the specified paint. 201 */ 202 public boolean draw(Canvas c, Paint p) { 203 final boolean canUseHardware = c.isHardwareAccelerated(); 204 if (mCanUseHardware != canUseHardware && mCanUseHardware) { 205 // We've switched from hardware to non-hardware mode. Panic. 206 cancelHardwareAnimations(); 207 } 208 mCanUseHardware = canUseHardware; 209 210 final boolean hasContent; 211 if (canUseHardware && mHardwareAnimating) { 212 hasContent = drawHardware((HardwareCanvas) c); 213 } else { 214 hasContent = drawSoftware(c, p); 215 } 216 217 return hasContent; 218 } 219 220 private boolean drawHardware(HardwareCanvas c) { 221 // If we have any pending hardware animations, cancel any running 222 // animations and start those now. 223 final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations; 224 final int N = pendingAnimations.size(); 225 if (N > 0) { 226 cancelHardwareAnimations(); 227 228 for (int i = 0; i < N; i++) { 229 pendingAnimations.get(i).setTarget(c); 230 pendingAnimations.get(i).start(); 231 } 232 233 mRunningAnimations.addAll(pendingAnimations); 234 pendingAnimations.clear(); 235 } 236 237 c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint); 238 239 return true; 240 } 241 242 private boolean drawSoftware(Canvas c, Paint p) { 243 boolean hasContent = false; 244 245 // Cache the paint alpha so we can restore it later. 246 final int paintAlpha = p.getAlpha(); 247 248 final int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5f); 249 if (outerAlpha > 0 && mOuterRadius > 0) { 250 p.setAlpha(outerAlpha); 251 p.setStyle(Style.FILL); 252 c.drawCircle(mOuterX, mOuterY, mOuterRadius, p); 253 hasContent = true; 254 } 255 256 p.setAlpha(paintAlpha); 257 258 return hasContent; 259 } 260 261 /** 262 * Returns the maximum bounds of the ripple relative to the ripple center. 263 */ 264 public void getBounds(Rect bounds) { 265 final int outerX = (int) mOuterX; 266 final int outerY = (int) mOuterY; 267 final int r = (int) mOuterRadius; 268 bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); 269 } 270 271 /** 272 * Specifies the starting position relative to the drawable bounds. No-op if 273 * the ripple has already entered. 274 */ 275 public void move(float x, float y) { 276 mStartingX = x; 277 mStartingY = y; 278 279 clampStartingPosition(); 280 } 281 282 /** 283 * Starts the enter animation. 284 */ 285 public void enter() { 286 final int radiusDuration = (int) 287 (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5); 288 final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY_MIN); 289 290 final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1); 291 cX.setAutoCancel(true); 292 cX.setDuration(radiusDuration); 293 cX.setInterpolator(LINEAR_INTERPOLATOR); 294 cX.setStartDelay(RIPPLE_ENTER_DELAY); 295 296 final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1); 297 cY.setAutoCancel(true); 298 cY.setDuration(radiusDuration); 299 cY.setInterpolator(LINEAR_INTERPOLATOR); 300 cY.setStartDelay(RIPPLE_ENTER_DELAY); 301 302 final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1); 303 outer.setAutoCancel(true); 304 outer.setDuration(outerDuration); 305 outer.setInterpolator(LINEAR_INTERPOLATOR); 306 307 mAnimOuterOpacity = outer; 308 mAnimX = cX; 309 mAnimY = cY; 310 311 // Enter animations always run on the UI thread, since it's unlikely 312 // that anything interesting is happening until the user lifts their 313 // finger. 314 outer.start(); 315 cX.start(); 316 cY.start(); 317 } 318 319 /** 320 * Starts the exit animation. 321 */ 322 public void exit() { 323 cancelSoftwareAnimations(); 324 325 // Scale the outer max opacity and opacity velocity based 326 // on the size of the outer radius. 327 final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); 328 final float outerSizeInfluence = MathUtils.constrain( 329 (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity) 330 / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1); 331 final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_VELOCITY_MIN, 332 WAVE_OUTER_OPACITY_VELOCITY_MAX, outerSizeInfluence); 333 334 // Determine at what time the inner and outer opacity intersect. 335 // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000 336 // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000 337 final int outerInflection = Math.max(0, (int) (1000 * (1 - mOuterOpacity) 338 / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f)); 339 final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection 340 * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f); 341 342 if (mCanUseHardware) { 343 exitHardware(opacityDuration, outerInflection, inflectionOpacity); 344 } else { 345 exitSoftware(opacityDuration, outerInflection, inflectionOpacity); 346 } 347 } 348 349 private void exitHardware(int opacityDuration, int outerInflection, int inflectionOpacity) { 350 mPendingAnimations.clear(); 351 352 // TODO: Adjust background by starting position. 353 final float startX = MathUtils.lerp( 354 mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); 355 final float startY = MathUtils.lerp( 356 mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); 357 358 final Paint outerPaint = getTempPaint(); 359 outerPaint.setAntiAlias(true); 360 outerPaint.setColor(mColor); 361 outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f)); 362 outerPaint.setStyle(Style.FILL); 363 mPropOuterPaint = CanvasProperty.createPaint(outerPaint); 364 mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius); 365 mPropOuterX = CanvasProperty.createFloat(mOuterX); 366 mPropOuterY = CanvasProperty.createFloat(mOuterY); 367 368 final RenderNodeAnimator outerOpacityAnim; 369 if (outerInflection > 0) { 370 // Outer opacity continues to increase for a bit. 371 outerOpacityAnim = new RenderNodeAnimator( 372 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity); 373 outerOpacityAnim.setDuration(outerInflection); 374 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 375 376 // Chain the outer opacity exit animation. 377 final int outerDuration = opacityDuration - outerInflection; 378 if (outerDuration > 0) { 379 final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator( 380 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); 381 outerFadeOutAnim.setDuration(outerDuration); 382 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); 383 outerFadeOutAnim.setStartDelay(outerInflection); 384 outerFadeOutAnim.setStartValue(inflectionOpacity); 385 outerFadeOutAnim.addListener(mAnimationListener); 386 387 mPendingAnimations.add(outerFadeOutAnim); 388 } else { 389 outerOpacityAnim.addListener(mAnimationListener); 390 } 391 } else { 392 outerOpacityAnim = new RenderNodeAnimator( 393 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); 394 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 395 outerOpacityAnim.setDuration(opacityDuration); 396 outerOpacityAnim.addListener(mAnimationListener); 397 } 398 399 mPendingAnimations.add(outerOpacityAnim); 400 401 mHardwareAnimating = true; 402 403 invalidateSelf(); 404 } 405 406 private Paint getTempPaint() { 407 if (mTempPaint == null) { 408 mTempPaint = new Paint(); 409 } 410 return mTempPaint; 411 } 412 413 private void exitSoftware(int opacityDuration, int outerInflection, int inflectionOpacity) { 414 final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1); 415 xAnim.setAutoCancel(true); 416 xAnim.setDuration(opacityDuration); 417 xAnim.setInterpolator(DECEL_INTERPOLATOR); 418 419 final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1); 420 yAnim.setAutoCancel(true); 421 yAnim.setDuration(opacityDuration); 422 yAnim.setInterpolator(DECEL_INTERPOLATOR); 423 424 final ObjectAnimator outerOpacityAnim; 425 if (outerInflection > 0) { 426 // Outer opacity continues to increase for a bit. 427 outerOpacityAnim = ObjectAnimator.ofFloat(this, 428 "outerOpacity", inflectionOpacity / 255.0f); 429 outerOpacityAnim.setAutoCancel(true); 430 outerOpacityAnim.setDuration(outerInflection); 431 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 432 433 // Chain the outer opacity exit animation. 434 final int outerDuration = opacityDuration - outerInflection; 435 if (outerDuration > 0) { 436 outerOpacityAnim.addListener(new AnimatorListenerAdapter() { 437 @Override 438 public void onAnimationEnd(Animator animation) { 439 final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat( 440 RippleBackground.this, "outerOpacity", 0); 441 outerFadeOutAnim.setAutoCancel(true); 442 outerFadeOutAnim.setDuration(outerDuration); 443 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); 444 outerFadeOutAnim.addListener(mAnimationListener); 445 446 mAnimOuterOpacity = outerFadeOutAnim; 447 448 outerFadeOutAnim.start(); 449 } 450 451 @Override 452 public void onAnimationCancel(Animator animation) { 453 animation.removeListener(this); 454 } 455 }); 456 } else { 457 outerOpacityAnim.addListener(mAnimationListener); 458 } 459 } else { 460 outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0); 461 outerOpacityAnim.setAutoCancel(true); 462 outerOpacityAnim.setDuration(opacityDuration); 463 outerOpacityAnim.addListener(mAnimationListener); 464 } 465 466 mAnimOuterOpacity = outerOpacityAnim; 467 mAnimX = xAnim; 468 mAnimY = yAnim; 469 470 outerOpacityAnim.start(); 471 xAnim.start(); 472 yAnim.start(); 473 } 474 475 /** 476 * Cancel all animations. 477 */ 478 public void cancel() { 479 cancelSoftwareAnimations(); 480 cancelHardwareAnimations(); 481 } 482 483 private void cancelSoftwareAnimations() { 484 if (mAnimOuterOpacity != null) { 485 mAnimOuterOpacity.cancel(); 486 } 487 488 if (mAnimX != null) { 489 mAnimX.cancel(); 490 } 491 492 if (mAnimY != null) { 493 mAnimY.cancel(); 494 } 495 } 496 497 /** 498 * Cancels any running hardware animations. 499 */ 500 private void cancelHardwareAnimations() { 501 final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; 502 final int N = runningAnimations.size(); 503 for (int i = 0; i < N; i++) { 504 runningAnimations.get(i).cancel(); 505 } 506 507 runningAnimations.clear(); 508 } 509 510 private void removeSelf() { 511 // The owner will invalidate itself. 512 mOwner.removeBackground(this); 513 } 514 515 private void invalidateSelf() { 516 mOwner.invalidateSelf(); 517 } 518 519 private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { 520 @Override 521 public void onAnimationEnd(Animator animation) { 522 removeSelf(); 523 } 524 }; 525 526 /** 527 * Interpolator with a smooth log deceleration 528 */ 529 private static final class LogInterpolator implements TimeInterpolator { 530 @Override 531 public float getInterpolation(float input) { 532 return 1 - (float) Math.pow(400, -input * 1.4); 533 } 534 } 535} 536