RippleBackground.java revision a3f0c2b21a73a82a919abe247c4046d114f3712c
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.Color; 26import android.graphics.Paint; 27import android.graphics.Paint.Style; 28import android.graphics.Rect; 29import android.util.MathUtils; 30import android.view.HardwareCanvas; 31import android.view.RenderNodeAnimator; 32import android.view.animation.LinearInterpolator; 33 34import java.util.ArrayList; 35 36/** 37 * Draws a Material ripple. 38 */ 39class RippleBackground { 40 private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 41 42 private static final float GLOBAL_SPEED = 1.0f; 43 private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED; 44 private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED; 45 private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED; 46 private static final float WAVE_OUTER_OPACITY_ENTER_VELOCITY = 10.0f * 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 // Hardware animators. 51 private final ArrayList<RenderNodeAnimator> mRunningAnimations = 52 new ArrayList<RenderNodeAnimator>(); 53 private final ArrayList<RenderNodeAnimator> mPendingAnimations = 54 new ArrayList<RenderNodeAnimator>(); 55 56 private final RippleDrawable mOwner; 57 58 /** Bounds used for computing max radius. */ 59 private final Rect mBounds; 60 61 /** Full-opacity color for drawing this ripple. */ 62 private int mColorOpaque; 63 64 /** Maximum alpha value for drawing this ripple. */ 65 private int mColorAlpha; 66 67 /** Maximum ripple radius. */ 68 private float mOuterRadius; 69 70 /** Screen density used to adjust pixel-based velocities. */ 71 private float mDensity; 72 73 // Hardware rendering properties. 74 private CanvasProperty<Paint> mPropOuterPaint; 75 private CanvasProperty<Float> mPropOuterRadius; 76 private CanvasProperty<Float> mPropOuterX; 77 private CanvasProperty<Float> mPropOuterY; 78 79 // Software animators. 80 private ObjectAnimator mAnimOuterOpacity; 81 82 // Temporary paint used for creating canvas properties. 83 private Paint mTempPaint; 84 85 // Software rendering properties. 86 private float mOuterOpacity = 0; 87 private float mOuterX; 88 private float mOuterY; 89 90 /** Whether we should be drawing hardware animations. */ 91 private boolean mHardwareAnimating; 92 93 /** Whether we can use hardware acceleration for the exit animation. */ 94 private boolean mCanUseHardware; 95 96 /** Whether we have an explicit maximum radius. */ 97 private boolean mHasMaxRadius; 98 99 /** 100 * Creates a new ripple. 101 */ 102 public RippleBackground(RippleDrawable owner, Rect bounds) { 103 mOwner = owner; 104 mBounds = bounds; 105 } 106 107 public void setup(int maxRadius, int color, float density) { 108 mColorOpaque = color | 0xFF000000; 109 mColorAlpha = Color.alpha(color); 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 public boolean shouldDraw() { 166 final int outerAlpha = (int) (mColorAlpha * mOuterOpacity + 0.5f); 167 return mCanUseHardware && mHardwareAnimating || outerAlpha > 0 && mOuterRadius > 0; 168 } 169 170 private boolean drawHardware(HardwareCanvas c) { 171 // If we have any pending hardware animations, cancel any running 172 // animations and start those now. 173 final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations; 174 final int N = pendingAnimations.size(); 175 if (N > 0) { 176 cancelHardwareAnimations(false); 177 178 // We canceled old animations, but we're about to run new ones. 179 mHardwareAnimating = true; 180 181 for (int i = 0; i < N; i++) { 182 pendingAnimations.get(i).setTarget(c); 183 pendingAnimations.get(i).start(); 184 } 185 186 mRunningAnimations.addAll(pendingAnimations); 187 pendingAnimations.clear(); 188 } 189 190 c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint); 191 192 return true; 193 } 194 195 private boolean drawSoftware(Canvas c, Paint p) { 196 boolean hasContent = false; 197 198 p.setColor(mColorOpaque); 199 final int outerAlpha = (int) (mColorAlpha * mOuterOpacity + 0.5f); 200 if (outerAlpha > 0 && mOuterRadius > 0) { 201 p.setAlpha(outerAlpha); 202 p.setStyle(Style.FILL); 203 c.drawCircle(mOuterX, mOuterY, mOuterRadius, p); 204 hasContent = true; 205 } 206 207 return hasContent; 208 } 209 210 /** 211 * Returns the maximum bounds of the ripple relative to the ripple center. 212 */ 213 public void getBounds(Rect bounds) { 214 final int outerX = (int) mOuterX; 215 final int outerY = (int) mOuterY; 216 final int r = (int) mOuterRadius + 1; 217 bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); 218 } 219 220 /** 221 * Starts the enter animation. 222 */ 223 public void enter() { 224 cancel(); 225 226 final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_ENTER_VELOCITY); 227 final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1); 228 outer.setAutoCancel(true); 229 outer.setDuration(outerDuration); 230 outer.setInterpolator(LINEAR_INTERPOLATOR); 231 232 mAnimOuterOpacity = outer; 233 234 // Enter animations always run on the UI thread, since it's unlikely 235 // that anything interesting is happening until the user lifts their 236 // finger. 237 outer.start(); 238 } 239 240 /** 241 * Starts the exit animation. 242 */ 243 public void exit() { 244 cancel(); 245 246 // Scale the outer max opacity and opacity velocity based 247 // on the size of the outer radius. 248 final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); 249 final float outerSizeInfluence = MathUtils.constrain( 250 (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity) 251 / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1); 252 final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN, 253 WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence); 254 255 // Determine at what time the inner and outer opacity intersect. 256 // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000 257 // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000 258 final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity) 259 / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f)); 260 final int inflectionOpacity = (int) (mColorAlpha * (mOuterOpacity 261 + inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f); 262 263 if (mCanUseHardware) { 264 exitHardware(opacityDuration, inflectionDuration, inflectionOpacity); 265 } else { 266 exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity); 267 } 268 } 269 270 private void exitHardware(int opacityDuration, int inflectionDuration, int inflectionOpacity) { 271 mPendingAnimations.clear(); 272 273 final Paint outerPaint = getTempPaint(); 274 outerPaint.setAntiAlias(true); 275 outerPaint.setColor(mColorOpaque); 276 outerPaint.setAlpha((int) (mColorAlpha * mOuterOpacity + 0.5f)); 277 outerPaint.setStyle(Style.FILL); 278 mPropOuterPaint = CanvasProperty.createPaint(outerPaint); 279 mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius); 280 mPropOuterX = CanvasProperty.createFloat(mOuterX); 281 mPropOuterY = CanvasProperty.createFloat(mOuterY); 282 283 final RenderNodeAnimator outerOpacityAnim; 284 if (inflectionDuration > 0) { 285 // Outer opacity continues to increase for a bit. 286 outerOpacityAnim = new RenderNodeAnimator(mPropOuterPaint, 287 RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity); 288 outerOpacityAnim.setDuration(inflectionDuration); 289 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 290 291 // Chain the outer opacity exit animation. 292 final int outerDuration = opacityDuration - inflectionDuration; 293 if (outerDuration > 0) { 294 final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator( 295 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); 296 outerFadeOutAnim.setDuration(outerDuration); 297 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); 298 outerFadeOutAnim.setStartDelay(inflectionDuration); 299 outerFadeOutAnim.setStartValue(inflectionOpacity); 300 outerFadeOutAnim.addListener(mAnimationListener); 301 302 mPendingAnimations.add(outerFadeOutAnim); 303 } else { 304 outerOpacityAnim.addListener(mAnimationListener); 305 } 306 } else { 307 outerOpacityAnim = new RenderNodeAnimator( 308 mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0); 309 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 310 outerOpacityAnim.setDuration(opacityDuration); 311 outerOpacityAnim.addListener(mAnimationListener); 312 } 313 314 mPendingAnimations.add(outerOpacityAnim); 315 316 mHardwareAnimating = true; 317 318 // Set up the software values to match the hardware end values. 319 mOuterOpacity = 0; 320 321 invalidateSelf(); 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() { 341 if (mTempPaint == null) { 342 mTempPaint = new Paint(); 343 } 344 return mTempPaint; 345 } 346 347 private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) { 348 final ObjectAnimator outerOpacityAnim; 349 if (inflectionDuration > 0) { 350 // Outer opacity continues to increase for a bit. 351 outerOpacityAnim = ObjectAnimator.ofFloat(this, 352 "outerOpacity", inflectionOpacity / 255.0f); 353 outerOpacityAnim.setAutoCancel(true); 354 outerOpacityAnim.setDuration(inflectionDuration); 355 outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR); 356 357 // Chain the outer opacity exit animation. 358 final int outerDuration = opacityDuration - inflectionDuration; 359 if (outerDuration > 0) { 360 outerOpacityAnim.addListener(new AnimatorListenerAdapter() { 361 @Override 362 public void onAnimationEnd(Animator animation) { 363 final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat( 364 RippleBackground.this, "outerOpacity", 0); 365 outerFadeOutAnim.setAutoCancel(true); 366 outerFadeOutAnim.setDuration(outerDuration); 367 outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR); 368 outerFadeOutAnim.addListener(mAnimationListener); 369 370 mAnimOuterOpacity = outerFadeOutAnim; 371 372 outerFadeOutAnim.start(); 373 } 374 375 @Override 376 public void onAnimationCancel(Animator animation) { 377 animation.removeListener(this); 378 } 379 }); 380 } else { 381 outerOpacityAnim.addListener(mAnimationListener); 382 } 383 } else { 384 outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0); 385 outerOpacityAnim.setAutoCancel(true); 386 outerOpacityAnim.setDuration(opacityDuration); 387 outerOpacityAnim.addListener(mAnimationListener); 388 } 389 390 mAnimOuterOpacity = outerOpacityAnim; 391 392 outerOpacityAnim.start(); 393 } 394 395 /** 396 * Cancel all animations. The caller is responsible for removing 397 * the ripple from the list of animating ripples. 398 */ 399 public void cancel() { 400 cancelSoftwareAnimations(); 401 cancelHardwareAnimations(true); 402 } 403 404 private void cancelSoftwareAnimations() { 405 if (mAnimOuterOpacity != null) { 406 mAnimOuterOpacity.cancel(); 407 mAnimOuterOpacity = null; 408 } 409 } 410 411 /** 412 * Cancels any running hardware animations. 413 */ 414 private void cancelHardwareAnimations(boolean cancelPending) { 415 final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations; 416 final int N = runningAnimations.size(); 417 for (int i = 0; i < N; i++) { 418 runningAnimations.get(i).cancel(); 419 } 420 runningAnimations.clear(); 421 422 if (cancelPending && !mPendingAnimations.isEmpty()) { 423 mPendingAnimations.clear(); 424 } 425 426 mHardwareAnimating = false; 427 } 428 429 private void invalidateSelf() { 430 mOwner.invalidateSelf(); 431 } 432 433 private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { 434 @Override 435 public void onAnimationEnd(Animator animation) { 436 mHardwareAnimating = false; 437 } 438 }; 439} 440