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 17 package android.graphics.drawable; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.animation.TimeInterpolator; 23 import android.graphics.Canvas; 24 import android.graphics.CanvasProperty; 25 import android.graphics.Color; 26 import android.graphics.Paint; 27 import android.graphics.Rect; 28 import android.util.MathUtils; 29 import android.view.HardwareCanvas; 30 import android.view.RenderNodeAnimator; 31 import android.view.animation.LinearInterpolator; 32 33 import java.util.ArrayList; 34 35 /** 36 * Draws a Material ripple. 37 */ 38 class 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_SIZE_INFLUENCE_MAX = 200f; 46 private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f; 47 48 private static final int ENTER_DURATION = 667; 49 private static final int ENTER_DURATION_FAST = 100; 50 51 // Hardware animators. 52 private final ArrayList<RenderNodeAnimator> mRunningAnimations = 53 new ArrayList<RenderNodeAnimator>(); 54 55 private final RippleDrawable mOwner; 56 57 /** Bounds used for computing max radius. */ 58 private final Rect mBounds; 59 60 /** ARGB 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 // Hardware rendering properties. 70 private CanvasProperty<Paint> mPropOuterPaint; 71 private CanvasProperty<Float> mPropOuterRadius; 72 private CanvasProperty<Float> mPropOuterX; 73 private CanvasProperty<Float> mPropOuterY; 74 75 // Software animators. 76 private ObjectAnimator mAnimOuterOpacity; 77 78 // Temporary paint used for creating canvas properties. 79 private Paint mTempPaint; 80 81 // Software rendering properties. 82 private float mOuterOpacity = 0; 83 private float mOuterX; 84 private float mOuterY; 85 86 /** Whether we should be drawing hardware animations. */ 87 private boolean mHardwareAnimating; 88 89 /** Whether we can use hardware acceleration for the exit animation. */ 90 private boolean mCanUseHardware; 91 92 /** Whether we have an explicit maximum radius. */ 93 private boolean mHasMaxRadius; 94 95 private boolean mHasPendingHardwareExit; 96 private int mPendingOpacityDuration; 97 private int mPendingInflectionDuration; 98 private int mPendingInflectionOpacity; 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, float density) { 109 if (maxRadius != RippleDrawable.RADIUS_AUTO) { 110 mHasMaxRadius = true; 111 mOuterRadius = maxRadius; 112 } else { 113 final float halfWidth = mBounds.width() / 2.0f; 114 final float halfHeight = mBounds.height() / 2.0f; 115 mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); 116 } 117 118 mOuterX = 0; 119 mOuterY = 0; 120 mDensity = density; 121 } 122 123 public void onHotspotBoundsChanged() { 124 if (!mHasMaxRadius) { 125 final float halfWidth = mBounds.width() / 2.0f; 126 final float halfHeight = mBounds.height() / 2.0f; 127 mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); 128 } 129 } 130 131 @SuppressWarnings("unused") 132 public void setOuterOpacity(float a) { 133 mOuterOpacity = a; 134 invalidateSelf(); 135 } 136 137 @SuppressWarnings("unused") 138 public float getOuterOpacity() { 139 return mOuterOpacity; 140 } 141 142 /** 143 * Draws the ripple centered at (0,0) using the specified paint. 144 */ 145 public boolean draw(Canvas c, Paint p) { 146 mColor = p.getColor(); 147 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 || mHasPendingHardwareExit)) { 157 hasContent = drawHardware((HardwareCanvas) c, p); 158 } else { 159 hasContent = drawSoftware(c, p); 160 } 161 162 return hasContent; 163 } 164 165 public boolean shouldDraw() { 166 return (mCanUseHardware && mHardwareAnimating) || (mOuterOpacity > 0 && mOuterRadius > 0); 167 } 168 169 private boolean drawHardware(HardwareCanvas c, Paint p) { 170 if (mHasPendingHardwareExit) { 171 cancelHardwareAnimations(false); 172 startPendingHardwareExit(c, p); 173 } 174 175 c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint); 176 177 return true; 178 } 179 180 private boolean drawSoftware(Canvas c, Paint p) { 181 boolean hasContent = false; 182 183 final int paintAlpha = p.getAlpha(); 184 final int alpha = (int) (paintAlpha * mOuterOpacity + 0.5f); 185 final float radius = mOuterRadius; 186 if (alpha > 0 && radius > 0) { 187 p.setAlpha(alpha); 188 c.drawCircle(mOuterX, mOuterY, radius, p); 189 p.setAlpha(paintAlpha); 190 hasContent = true; 191 } 192 193 return hasContent; 194 } 195 196 /** 197 * Returns the maximum bounds of the ripple relative to the ripple center. 198 */ 199 public void getBounds(Rect bounds) { 200 final int outerX = (int) mOuterX; 201 final int outerY = (int) mOuterY; 202 final int r = (int) mOuterRadius + 1; 203 bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); 204 } 205 206 /** 207 * Starts the enter animation. 208 */ 209 public void enter(boolean fast) { 210 cancel(); 211 212 final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1); 213 opacity.setAutoCancel(true); 214 opacity.setDuration(fast ? ENTER_DURATION_FAST : ENTER_DURATION); 215 opacity.setInterpolator(LINEAR_INTERPOLATOR); 216 217 mAnimOuterOpacity = opacity; 218 219 // Enter animations always run on the UI thread, since it's unlikely 220 // that anything interesting is happening until the user lifts their 221 // finger. 222 opacity.start(); 223 } 224 225 /** 226 * Starts the exit animation. 227 */ 228 public void exit() { 229 cancel(); 230 231 // Scale the outer max opacity and opacity velocity based 232 // on the size of the outer radius. 233 final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); 234 final float outerSizeInfluence = MathUtils.constrain( 235 (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity) 236 / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1); 237 final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN, 238 WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence); 239 240 // Determine at what time the inner and outer opacity intersect. 241 // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000 242 // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000 243 final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity) 244 / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f)); 245 final int inflectionOpacity = (int) (Color.alpha(mColor) * (mOuterOpacity 246 + inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f); 247 248 if (mCanUseHardware) { 249 createPendingHardwareExit(opacityDuration, inflectionDuration, inflectionOpacity); 250 } else { 251 exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity); 252 } 253 } 254 255 private void createPendingHardwareExit( 256 int opacityDuration, int inflectionDuration, int inflectionOpacity) { 257 mHasPendingHardwareExit = true; 258 mPendingOpacityDuration = opacityDuration; 259 mPendingInflectionDuration = inflectionDuration; 260 mPendingInflectionOpacity = inflectionOpacity; 261 262 // The animation will start on the next draw(). 263 invalidateSelf(); 264 } 265 266 private void startPendingHardwareExit(HardwareCanvas c, Paint p) { 267 mHasPendingHardwareExit = false; 268 269 final int opacityDuration = mPendingOpacityDuration; 270 final int inflectionDuration = mPendingInflectionDuration; 271 final int inflectionOpacity = mPendingInflectionOpacity; 272 273 final Paint outerPaint = getTempPaint(p); 274 outerPaint.setAlpha((int) (outerPaint.getAlpha() * mOuterOpacity + 0.5f)); 275 mPropOuterPaint = CanvasProperty.createPaint(outerPaint); 276 mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius); 277 mPropOuterX = CanvasProperty.createFloat(mOuterX); 278 mPropOuterY = CanvasProperty.createFloat(mOuterY); 279 280 final RenderNodeAnimator outerOpacityAnim; 281 if (inflectionDuration > 0) { 282 // Outer opacity continues to increase for a bit. 283 outerOpacityAnim = new RenderNodeAnimator(mPropOuterPaint, 284 RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity); 285 outerOpacityAnim.setDuration(inflectionDuration); 286 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 287 288 // Chain the outer opacity exit animation. 289 final int outerDuration = opacityDuration - inflectionDuration; 290 if (outerDuration > 0) { 291 final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator( 292 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); 293 outerFadeOutAnim.setDuration(outerDuration); 294 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); 295 outerFadeOutAnim.setStartDelay(inflectionDuration); 296 outerFadeOutAnim.setStartValue(inflectionOpacity); 297 outerFadeOutAnim.addListener(mAnimationListener); 298 outerFadeOutAnim.setTarget(c); 299 outerFadeOutAnim.start(); 300 301 mRunningAnimations.add(outerFadeOutAnim); 302 } else { 303 outerOpacityAnim.addListener(mAnimationListener); 304 } 305 } else { 306 outerOpacityAnim = new RenderNodeAnimator( 307 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); 308 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 309 outerOpacityAnim.setDuration(opacityDuration); 310 outerOpacityAnim.addListener(mAnimationListener); 311 } 312 313 outerOpacityAnim.setTarget(c); 314 outerOpacityAnim.start(); 315 316 mRunningAnimations.add(outerOpacityAnim); 317 318 mHardwareAnimating = true; 319 320 // Set up the software values to match the hardware end values. 321 mOuterOpacity = 0; 322 } 323 324 /** 325 * Jump all animations to their end state. The caller is responsible for 326 * removing the ripple from the list of animating ripples. 327 */ 328 public void jump() { 329 endSoftwareAnimations(); 330 cancelHardwareAnimations(true); 331 } 332 333 private void endSoftwareAnimations() { 334 if (mAnimOuterOpacity != null) { 335 mAnimOuterOpacity.end(); 336 mAnimOuterOpacity = null; 337 } 338 } 339 340 private Paint getTempPaint(Paint original) { 341 if (mTempPaint == null) { 342 mTempPaint = new Paint(); 343 } 344 mTempPaint.set(original); 345 return mTempPaint; 346 } 347 348 private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) { 349 final ObjectAnimator outerOpacityAnim; 350 if (inflectionDuration > 0) { 351 // Outer opacity continues to increase for a bit. 352 outerOpacityAnim = ObjectAnimator.ofFloat(this, 353 "outerOpacity", inflectionOpacity / 255.0f); 354 outerOpacityAnim.setAutoCancel(true); 355 outerOpacityAnim.setDuration(inflectionDuration); 356 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 357 358 // Chain the outer opacity exit animation. 359 final int outerDuration = opacityDuration - inflectionDuration; 360 if (outerDuration > 0) { 361 outerOpacityAnim.addListener(new AnimatorListenerAdapter() { 362 @Override 363 public void onAnimationEnd(Animator animation) { 364 final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat( 365 RippleBackground.this, "outerOpacity", 0); 366 outerFadeOutAnim.setAutoCancel(true); 367 outerFadeOutAnim.setDuration(outerDuration); 368 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); 369 outerFadeOutAnim.addListener(mAnimationListener); 370 371 mAnimOuterOpacity = outerFadeOutAnim; 372 373 outerFadeOutAnim.start(); 374 } 375 376 @Override 377 public void onAnimationCancel(Animator animation) { 378 animation.removeListener(this); 379 } 380 }); 381 } else { 382 outerOpacityAnim.addListener(mAnimationListener); 383 } 384 } else { 385 outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0); 386 outerOpacityAnim.setAutoCancel(true); 387 outerOpacityAnim.setDuration(opacityDuration); 388 outerOpacityAnim.addListener(mAnimationListener); 389 } 390 391 mAnimOuterOpacity = outerOpacityAnim; 392 393 outerOpacityAnim.start(); 394 } 395 396 /** 397 * Cancel all animations. The caller is responsible for removing 398 * the ripple from the list of animating ripples. 399 */ 400 public void cancel() { 401 cancelSoftwareAnimations(); 402 cancelHardwareAnimations(false); 403 } 404 405 private void cancelSoftwareAnimations() { 406 if (mAnimOuterOpacity != null) { 407 mAnimOuterOpacity.cancel(); 408 mAnimOuterOpacity = null; 409 } 410 } 411 412 /** 413 * Cancels any running hardware animations. 414 */ 415 private void cancelHardwareAnimations(boolean jumpToEnd) { 416 final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; 417 final int N = runningAnimations.size(); 418 for (int i = 0; i < N; i++) { 419 if (jumpToEnd) { 420 runningAnimations.get(i).end(); 421 } else { 422 runningAnimations.get(i).cancel(); 423 } 424 } 425 runningAnimations.clear(); 426 427 if (mHasPendingHardwareExit) { 428 // If we had a pending hardware exit, jump to the end state. 429 mHasPendingHardwareExit = false; 430 431 if (jumpToEnd) { 432 mOuterOpacity = 0; 433 } 434 } 435 436 mHardwareAnimating = false; 437 } 438 439 private void invalidateSelf() { 440 mOwner.invalidateSelf(); 441 } 442 443 private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { 444 @Override 445 public void onAnimationEnd(Animator animation) { 446 mHardwareAnimating = false; 447 } 448 }; 449 } 450