RippleBackground.java revision a8a8ff000b2902eb4e187e62be39fd9535c6c839
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 41 private static final float GLOBAL_SPEED = 1.0f; 42 private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED; 43 private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED; 44 private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED; 45 private static final float WAVE_OUTER_OPACITY_ENTER_VELOCITY = 10.0f * GLOBAL_SPEED; 46 private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f; 47 private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f; 48 49 private static final long RIPPLE_ENTER_DELAY = 80; 50 51 // Hardware animators. 52 private final ArrayList<RenderNodeAnimator> mRunningAnimations = 53 new ArrayList<RenderNodeAnimator>(); 54 private final ArrayList<RenderNodeAnimator> mPendingAnimations = 55 new ArrayList<RenderNodeAnimator>(); 56 57 private final RippleDrawable mOwner; 58 59 /** Bounds used for computing max radius. */ 60 private final Rect mBounds; 61 62 /** Full-opacity color for drawing this ripple. */ 63 private int mColor; 64 65 /** Maximum ripple radius. */ 66 private float mOuterRadius; 67 68 /** Screen density used to adjust pixel-based velocities. */ 69 private float mDensity; 70 71 // Hardware rendering properties. 72 private CanvasProperty<Paint> mPropOuterPaint; 73 private CanvasProperty<Float> mPropOuterRadius; 74 private CanvasProperty<Float> mPropOuterX; 75 private CanvasProperty<Float> mPropOuterY; 76 77 // Software animators. 78 private ObjectAnimator mAnimOuterOpacity; 79 80 // Temporary paint used for creating canvas properties. 81 private Paint mTempPaint; 82 83 // Software rendering properties. 84 private float mOuterOpacity = 0; 85 private float mOuterX; 86 private float mOuterY; 87 88 /** Whether we should be drawing hardware animations. */ 89 private boolean mHardwareAnimating; 90 91 /** Whether we can use hardware acceleration for the exit animation. */ 92 private boolean mCanUseHardware; 93 94 /** Whether we have an explicit maximum radius. */ 95 private boolean mHasMaxRadius; 96 97 /** Whether we were canceled externally and should avoid self-removal. */ 98 private boolean mCanceled; 99 100 /** 101 * Creates a new ripple. 102 */ 103 public RippleBackground(RippleDrawable owner, Rect bounds) { 104 mOwner = owner; 105 mBounds = bounds; 106 } 107 108 public void setup(int maxRadius, int color, float density) { 109 mColor = color | 0xFF000000; 110 111 if (maxRadius != RippleDrawable.RADIUS_AUTO) { 112 mHasMaxRadius = true; 113 mOuterRadius = maxRadius; 114 } else { 115 final float halfWidth = mBounds.width() / 2.0f; 116 final float halfHeight = mBounds.height() / 2.0f; 117 mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); 118 } 119 120 mOuterX = 0; 121 mOuterY = 0; 122 mDensity = density; 123 } 124 125 public void onHotspotBoundsChanged() { 126 if (!mHasMaxRadius) { 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 133 @SuppressWarnings("unused") 134 public void setOuterOpacity(float a) { 135 mOuterOpacity = a; 136 invalidateSelf(); 137 } 138 139 @SuppressWarnings("unused") 140 public float getOuterOpacity() { 141 return mOuterOpacity; 142 } 143 144 /** 145 * Draws the ripple centered at (0,0) using the specified paint. 146 */ 147 public boolean draw(Canvas c, Paint p) { 148 final boolean canUseHardware = c.isHardwareAccelerated(); 149 if (mCanUseHardware != canUseHardware && mCanUseHardware) { 150 // We've switched from hardware to non-hardware mode. Panic. 151 cancelHardwareAnimations(true); 152 } 153 mCanUseHardware = canUseHardware; 154 155 final boolean hasContent; 156 if (canUseHardware && mHardwareAnimating) { 157 hasContent = drawHardware((HardwareCanvas) c); 158 } else { 159 hasContent = drawSoftware(c, p); 160 } 161 162 return hasContent; 163 } 164 165 private boolean drawHardware(HardwareCanvas c) { 166 // If we have any pending hardware animations, cancel any running 167 // animations and start those now. 168 final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations; 169 final int N = pendingAnimations.size(); 170 if (N > 0) { 171 cancelHardwareAnimations(false); 172 173 for (int i = 0; i < N; i++) { 174 pendingAnimations.get(i).setTarget(c); 175 pendingAnimations.get(i).start(); 176 } 177 178 mRunningAnimations.addAll(pendingAnimations); 179 pendingAnimations.clear(); 180 } 181 182 c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint); 183 184 return true; 185 } 186 187 private boolean drawSoftware(Canvas c, Paint p) { 188 boolean hasContent = false; 189 190 // Cache the paint alpha so we can restore it later. 191 final int paintAlpha = p.getAlpha(); 192 193 final int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5f); 194 if (outerAlpha > 0 && mOuterRadius > 0) { 195 p.setAlpha(outerAlpha); 196 p.setStyle(Style.FILL); 197 c.drawCircle(mOuterX, mOuterY, mOuterRadius, p); 198 hasContent = true; 199 } 200 201 p.setAlpha(paintAlpha); 202 203 return hasContent; 204 } 205 206 /** 207 * Returns the maximum bounds of the ripple relative to the ripple center. 208 */ 209 public void getBounds(Rect bounds) { 210 final int outerX = (int) mOuterX; 211 final int outerY = (int) mOuterY; 212 final int r = (int) mOuterRadius + 1; 213 bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); 214 } 215 216 /** 217 * Starts the enter animation. 218 */ 219 public void enter() { 220 cancel(); 221 222 final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_ENTER_VELOCITY); 223 final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1); 224 outer.setAutoCancel(true); 225 outer.setDuration(outerDuration); 226 outer.setInterpolator(LINEAR_INTERPOLATOR); 227 228 mAnimOuterOpacity = outer; 229 230 // Enter animations always run on the UI thread, since it's unlikely 231 // that anything interesting is happening until the user lifts their 232 // finger. 233 outer.start(); 234 } 235 236 /** 237 * Starts the exit animation. 238 */ 239 public void exit() { 240 cancel(); 241 242 // Scale the outer max opacity and opacity velocity based 243 // on the size of the outer radius. 244 final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); 245 final float outerSizeInfluence = MathUtils.constrain( 246 (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity) 247 / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1); 248 final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN, 249 WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence); 250 251 // Determine at what time the inner and outer opacity intersect. 252 // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000 253 // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000 254 final int outerInflection = Math.max(0, (int) (1000 * (1 - mOuterOpacity) 255 / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f)); 256 final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection 257 * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f); 258 259 if (mCanUseHardware) { 260 exitHardware(opacityDuration, outerInflection, inflectionOpacity); 261 } else { 262 exitSoftware(opacityDuration, outerInflection, inflectionOpacity); 263 } 264 } 265 266 private void exitHardware(int opacityDuration, int outerInflection, int inflectionOpacity) { 267 mPendingAnimations.clear(); 268 269 final Paint outerPaint = getTempPaint(); 270 outerPaint.setAntiAlias(true); 271 outerPaint.setColor(mColor); 272 outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f)); 273 outerPaint.setStyle(Style.FILL); 274 mPropOuterPaint = CanvasProperty.createPaint(outerPaint); 275 mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius); 276 mPropOuterX = CanvasProperty.createFloat(mOuterX); 277 mPropOuterY = CanvasProperty.createFloat(mOuterY); 278 279 final RenderNodeAnimator outerOpacityAnim; 280 if (outerInflection > 0) { 281 // Outer opacity continues to increase for a bit. 282 outerOpacityAnim = new RenderNodeAnimator( 283 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity); 284 outerOpacityAnim.setDuration(outerInflection); 285 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 286 287 // Chain the outer opacity exit animation. 288 final int outerDuration = opacityDuration - outerInflection; 289 if (outerDuration > 0) { 290 final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator( 291 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); 292 outerFadeOutAnim.setDuration(outerDuration); 293 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); 294 outerFadeOutAnim.setStartDelay(outerInflection); 295 outerFadeOutAnim.setStartValue(inflectionOpacity); 296 outerFadeOutAnim.addListener(mAnimationListener); 297 298 mPendingAnimations.add(outerFadeOutAnim); 299 } else { 300 outerOpacityAnim.addListener(mAnimationListener); 301 } 302 } else { 303 outerOpacityAnim = new RenderNodeAnimator( 304 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); 305 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 306 outerOpacityAnim.setDuration(opacityDuration); 307 outerOpacityAnim.addListener(mAnimationListener); 308 } 309 310 mPendingAnimations.add(outerOpacityAnim); 311 312 mHardwareAnimating = true; 313 314 invalidateSelf(); 315 } 316 317 /** 318 * Jump all animations to their end state. The caller is responsible for 319 * removing the ripple from the list of animating ripples. 320 */ 321 public void jump() { 322 mCanceled = true; 323 endSoftwareAnimations(); 324 endHardwareAnimations(); 325 mCanceled = false; 326 } 327 328 private void endSoftwareAnimations() { 329 if (mAnimOuterOpacity != null) { 330 mAnimOuterOpacity.end(); 331 } 332 } 333 334 private void endHardwareAnimations() { 335 final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; 336 final int N = runningAnimations.size(); 337 for (int i = 0; i < N; i++) { 338 runningAnimations.get(i).end(); 339 } 340 runningAnimations.clear(); 341 342 // Abort any pending animations. Since we always have a completion 343 // listener on a pending animation, we also need to remove ourselves. 344 if (!mPendingAnimations.isEmpty()) { 345 mPendingAnimations.clear(); 346 removeSelf(); 347 } 348 349 mHardwareAnimating = false; 350 } 351 352 private Paint getTempPaint() { 353 if (mTempPaint == null) { 354 mTempPaint = new Paint(); 355 } 356 return mTempPaint; 357 } 358 359 private void exitSoftware(int opacityDuration, int outerInflection, int inflectionOpacity) { 360 final ObjectAnimator outerOpacityAnim; 361 if (outerInflection > 0) { 362 // Outer opacity continues to increase for a bit. 363 outerOpacityAnim = ObjectAnimator.ofFloat(this, 364 "outerOpacity", inflectionOpacity / 255.0f); 365 outerOpacityAnim.setAutoCancel(true); 366 outerOpacityAnim.setDuration(outerInflection); 367 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 368 369 // Chain the outer opacity exit animation. 370 final int outerDuration = opacityDuration - outerInflection; 371 if (outerDuration > 0) { 372 outerOpacityAnim.addListener(new AnimatorListenerAdapter() { 373 @Override 374 public void onAnimationEnd(Animator animation) { 375 final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat( 376 RippleBackground.this, "outerOpacity", 0); 377 outerFadeOutAnim.setAutoCancel(true); 378 outerFadeOutAnim.setDuration(outerDuration); 379 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); 380 outerFadeOutAnim.addListener(mAnimationListener); 381 382 mAnimOuterOpacity = outerFadeOutAnim; 383 384 outerFadeOutAnim.start(); 385 } 386 387 @Override 388 public void onAnimationCancel(Animator animation) { 389 animation.removeListener(this); 390 } 391 }); 392 } else { 393 outerOpacityAnim.addListener(mAnimationListener); 394 } 395 } else { 396 outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0); 397 outerOpacityAnim.setAutoCancel(true); 398 outerOpacityAnim.setDuration(opacityDuration); 399 outerOpacityAnim.addListener(mAnimationListener); 400 } 401 402 mAnimOuterOpacity = outerOpacityAnim; 403 404 outerOpacityAnim.start(); 405 } 406 407 /** 408 * Cancel all animations. The caller is responsible for removing 409 * the ripple from the list of animating ripples. 410 */ 411 public void cancel() { 412 mCanceled = true; 413 cancelSoftwareAnimations(); 414 cancelHardwareAnimations(true); 415 mCanceled = false; 416 } 417 418 private void cancelSoftwareAnimations() { 419 if (mAnimOuterOpacity != null) { 420 mAnimOuterOpacity.cancel(); 421 } 422 } 423 424 /** 425 * Cancels any running hardware animations. 426 */ 427 private void cancelHardwareAnimations(boolean cancelPending) { 428 final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; 429 final int N = runningAnimations.size(); 430 for (int i = 0; i < N; i++) { 431 runningAnimations.get(i).cancel(); 432 } 433 434 runningAnimations.clear(); 435 436 if (cancelPending && !mPendingAnimations.isEmpty()) { 437 mPendingAnimations.clear(); 438 } 439 440 mHardwareAnimating = false; 441 } 442 443 private void removeSelf() { 444 // The owner will invalidate itself. 445 if (!mCanceled) { 446 mOwner.removeBackground(this); 447 } 448 } 449 450 private void invalidateSelf() { 451 mOwner.invalidateSelf(); 452 } 453 454 private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { 455 @Override 456 public void onAnimationEnd(Animator animation) { 457 removeSelf(); 458 } 459 }; 460 461 /** 462 * Interpolator with a smooth log deceleration 463 */ 464 private static final class LogInterpolator implements TimeInterpolator { 465 @Override 466 public float getInterpolation(float input) { 467 return 1 - (float) Math.pow(400, -input * 1.4); 468 } 469 } 470} 471