1 /* 2 * Copyright (C) 2015 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.Paint; 26 import android.graphics.RecordingCanvas; 27 import android.graphics.Rect; 28 import android.util.FloatProperty; 29 import android.util.MathUtils; 30 import android.view.RenderNodeAnimator; 31 import android.view.animation.AnimationUtils; 32 import android.view.animation.LinearInterpolator; 33 import android.view.animation.PathInterpolator; 34 35 import java.util.ArrayList; 36 37 /** 38 * Draws a ripple foreground. 39 */ 40 class RippleForeground extends RippleComponent { 41 private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 42 // Matches R.interpolator.fast_out_slow_in but as we have no context we can't just import that 43 private static final TimeInterpolator DECELERATE_INTERPOLATOR = 44 new PathInterpolator(0.4f, 0f, 0.2f, 1f); 45 46 // Time it takes for the ripple to expand 47 private static final int RIPPLE_ENTER_DURATION = 225; 48 // Time it takes for the ripple to slide from the touch to the center point 49 private static final int RIPPLE_ORIGIN_DURATION = 225; 50 51 private static final int OPACITY_ENTER_DURATION = 75; 52 private static final int OPACITY_EXIT_DURATION = 150; 53 private static final int OPACITY_HOLD_DURATION = OPACITY_ENTER_DURATION + 150; 54 55 // Parent-relative values for starting position. 56 private float mStartingX; 57 private float mStartingY; 58 private float mClampedStartingX; 59 private float mClampedStartingY; 60 61 // Hardware rendering properties. 62 private CanvasProperty<Paint> mPropPaint; 63 private CanvasProperty<Float> mPropRadius; 64 private CanvasProperty<Float> mPropX; 65 private CanvasProperty<Float> mPropY; 66 67 // Target values for tween animations. 68 private float mTargetX = 0; 69 private float mTargetY = 0; 70 71 // Software rendering properties. 72 private float mOpacity = 0; 73 74 // Values used to tween between the start and end positions. 75 private float mTweenRadius = 0; 76 private float mTweenX = 0; 77 private float mTweenY = 0; 78 79 /** Whether this ripple has finished its exit animation. */ 80 private boolean mHasFinishedExit; 81 82 /** Whether we can use hardware acceleration for the exit animation. */ 83 private boolean mUsingProperties; 84 85 private long mEnterStartedAtMillis; 86 87 private ArrayList<RenderNodeAnimator> mPendingHwAnimators = new ArrayList<>(); 88 private ArrayList<RenderNodeAnimator> mRunningHwAnimators = new ArrayList<>(); 89 90 private ArrayList<Animator> mRunningSwAnimators = new ArrayList<>(); 91 92 /** 93 * If set, force all ripple animations to not run on RenderThread, even if it would be 94 * available. 95 */ 96 private final boolean mForceSoftware; 97 98 /** 99 * If we have a bound, don't start from 0. Start from 60% of the max out of width and height. 100 */ 101 private float mStartRadius = 0; 102 RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY, boolean forceSoftware)103 public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY, 104 boolean forceSoftware) { 105 super(owner, bounds); 106 107 mForceSoftware = forceSoftware; 108 mStartingX = startingX; 109 mStartingY = startingY; 110 111 // Take 60% of the maximum of the width and height, then divided half to get the radius. 112 mStartRadius = Math.max(bounds.width(), bounds.height()) * 0.3f; 113 clampStartingPosition(); 114 } 115 116 @Override onTargetRadiusChanged(float targetRadius)117 protected void onTargetRadiusChanged(float targetRadius) { 118 clampStartingPosition(); 119 switchToUiThreadAnimation(); 120 } 121 drawSoftware(Canvas c, Paint p)122 private void drawSoftware(Canvas c, Paint p) { 123 final int origAlpha = p.getAlpha(); 124 final int alpha = (int) (origAlpha * mOpacity + 0.5f); 125 final float radius = getCurrentRadius(); 126 if (alpha > 0 && radius > 0) { 127 final float x = getCurrentX(); 128 final float y = getCurrentY(); 129 p.setAlpha(alpha); 130 c.drawCircle(x, y, radius, p); 131 p.setAlpha(origAlpha); 132 } 133 } 134 startPending(RecordingCanvas c)135 private void startPending(RecordingCanvas c) { 136 if (!mPendingHwAnimators.isEmpty()) { 137 for (int i = 0; i < mPendingHwAnimators.size(); i++) { 138 RenderNodeAnimator animator = mPendingHwAnimators.get(i); 139 animator.setTarget(c); 140 animator.start(); 141 mRunningHwAnimators.add(animator); 142 } 143 mPendingHwAnimators.clear(); 144 } 145 } 146 pruneHwFinished()147 private void pruneHwFinished() { 148 if (!mRunningHwAnimators.isEmpty()) { 149 for (int i = mRunningHwAnimators.size() - 1; i >= 0; i--) { 150 if (!mRunningHwAnimators.get(i).isRunning()) { 151 mRunningHwAnimators.remove(i); 152 } 153 } 154 } 155 } 156 pruneSwFinished()157 private void pruneSwFinished() { 158 if (!mRunningSwAnimators.isEmpty()) { 159 for (int i = mRunningSwAnimators.size() - 1; i >= 0; i--) { 160 if (!mRunningSwAnimators.get(i).isRunning()) { 161 mRunningSwAnimators.remove(i); 162 } 163 } 164 } 165 } 166 drawHardware(RecordingCanvas c, Paint p)167 private void drawHardware(RecordingCanvas c, Paint p) { 168 startPending(c); 169 pruneHwFinished(); 170 if (mPropPaint != null) { 171 mUsingProperties = true; 172 c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); 173 } else { 174 mUsingProperties = false; 175 drawSoftware(c, p); 176 } 177 } 178 179 /** 180 * Returns the maximum bounds of the ripple relative to the ripple center. 181 */ getBounds(Rect bounds)182 public void getBounds(Rect bounds) { 183 final int outerX = (int) mTargetX; 184 final int outerY = (int) mTargetY; 185 final int r = (int) mTargetRadius + 1; 186 bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); 187 } 188 189 /** 190 * Specifies the starting position relative to the drawable bounds. No-op if 191 * the ripple has already entered. 192 */ move(float x, float y)193 public void move(float x, float y) { 194 mStartingX = x; 195 mStartingY = y; 196 197 clampStartingPosition(); 198 } 199 200 /** 201 * @return {@code true} if this ripple has finished its exit animation 202 */ hasFinishedExit()203 public boolean hasFinishedExit() { 204 return mHasFinishedExit; 205 } 206 computeFadeOutDelay()207 private long computeFadeOutDelay() { 208 long timeSinceEnter = AnimationUtils.currentAnimationTimeMillis() - mEnterStartedAtMillis; 209 if (timeSinceEnter > 0 && timeSinceEnter < OPACITY_HOLD_DURATION) { 210 return OPACITY_HOLD_DURATION - timeSinceEnter; 211 } 212 return 0; 213 } 214 startSoftwareEnter()215 private void startSoftwareEnter() { 216 for (int i = 0; i < mRunningSwAnimators.size(); i++) { 217 mRunningSwAnimators.get(i).cancel(); 218 } 219 mRunningSwAnimators.clear(); 220 221 final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); 222 tweenRadius.setDuration(RIPPLE_ENTER_DURATION); 223 tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR); 224 tweenRadius.start(); 225 mRunningSwAnimators.add(tweenRadius); 226 227 final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); 228 tweenOrigin.setDuration(RIPPLE_ORIGIN_DURATION); 229 tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR); 230 tweenOrigin.start(); 231 mRunningSwAnimators.add(tweenOrigin); 232 233 final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); 234 opacity.setDuration(OPACITY_ENTER_DURATION); 235 opacity.setInterpolator(LINEAR_INTERPOLATOR); 236 opacity.start(); 237 mRunningSwAnimators.add(opacity); 238 } 239 startSoftwareExit()240 private void startSoftwareExit() { 241 final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0); 242 opacity.setDuration(OPACITY_EXIT_DURATION); 243 opacity.setInterpolator(LINEAR_INTERPOLATOR); 244 opacity.addListener(mAnimationListener); 245 opacity.setStartDelay(computeFadeOutDelay()); 246 opacity.start(); 247 mRunningSwAnimators.add(opacity); 248 } 249 startHardwareEnter()250 private void startHardwareEnter() { 251 if (mForceSoftware) { return; } 252 mPropX = CanvasProperty.createFloat(getCurrentX()); 253 mPropY = CanvasProperty.createFloat(getCurrentY()); 254 mPropRadius = CanvasProperty.createFloat(getCurrentRadius()); 255 final Paint paint = mOwner.getRipplePaint(); 256 mPropPaint = CanvasProperty.createPaint(paint); 257 258 final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius); 259 radius.setDuration(RIPPLE_ORIGIN_DURATION); 260 radius.setInterpolator(DECELERATE_INTERPOLATOR); 261 mPendingHwAnimators.add(radius); 262 263 final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX); 264 x.setDuration(RIPPLE_ORIGIN_DURATION); 265 x.setInterpolator(DECELERATE_INTERPOLATOR); 266 mPendingHwAnimators.add(x); 267 268 final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY); 269 y.setDuration(RIPPLE_ORIGIN_DURATION); 270 y.setInterpolator(DECELERATE_INTERPOLATOR); 271 mPendingHwAnimators.add(y); 272 273 final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, 274 RenderNodeAnimator.PAINT_ALPHA, paint.getAlpha()); 275 opacity.setDuration(OPACITY_ENTER_DURATION); 276 opacity.setInterpolator(LINEAR_INTERPOLATOR); 277 opacity.setStartValue(0); 278 mPendingHwAnimators.add(opacity); 279 280 invalidateSelf(); 281 } 282 startHardwareExit()283 private void startHardwareExit() { 284 // Only run a hardware exit if we had a hardware enter to continue from 285 if (mForceSoftware || mPropPaint == null) return; 286 287 final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, 288 RenderNodeAnimator.PAINT_ALPHA, 0); 289 opacity.setDuration(OPACITY_EXIT_DURATION); 290 opacity.setInterpolator(LINEAR_INTERPOLATOR); 291 opacity.addListener(mAnimationListener); 292 opacity.setStartDelay(computeFadeOutDelay()); 293 opacity.setStartValue(mOwner.getRipplePaint().getAlpha()); 294 mPendingHwAnimators.add(opacity); 295 invalidateSelf(); 296 } 297 298 /** 299 * Starts a ripple enter animation. 300 */ enter()301 public final void enter() { 302 mEnterStartedAtMillis = AnimationUtils.currentAnimationTimeMillis(); 303 startSoftwareEnter(); 304 startHardwareEnter(); 305 } 306 307 /** 308 * Starts a ripple exit animation. 309 */ exit()310 public final void exit() { 311 startSoftwareExit(); 312 startHardwareExit(); 313 } 314 getCurrentX()315 private float getCurrentX() { 316 return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX); 317 } 318 getCurrentY()319 private float getCurrentY() { 320 return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY); 321 } 322 getCurrentRadius()323 private float getCurrentRadius() { 324 return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius); 325 } 326 327 /** 328 * Draws the ripple to the canvas, inheriting the paint's color and alpha 329 * properties. 330 * 331 * @param c the canvas to which the ripple should be drawn 332 * @param p the paint used to draw the ripple 333 */ draw(Canvas c, Paint p)334 public void draw(Canvas c, Paint p) { 335 final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof RecordingCanvas; 336 337 pruneSwFinished(); 338 if (hasDisplayListCanvas) { 339 final RecordingCanvas hw = (RecordingCanvas) c; 340 drawHardware(hw, p); 341 } else { 342 drawSoftware(c, p); 343 } 344 } 345 346 /** 347 * Clamps the starting position to fit within the ripple bounds. 348 */ clampStartingPosition()349 private void clampStartingPosition() { 350 final float cX = mBounds.exactCenterX(); 351 final float cY = mBounds.exactCenterY(); 352 final float dX = mStartingX - cX; 353 final float dY = mStartingY - cY; 354 final float r = mTargetRadius - mStartRadius; 355 if (dX * dX + dY * dY > r * r) { 356 // Point is outside the circle, clamp to the perimeter. 357 final double angle = Math.atan2(dY, dX); 358 mClampedStartingX = cX + (float) (Math.cos(angle) * r); 359 mClampedStartingY = cY + (float) (Math.sin(angle) * r); 360 } else { 361 mClampedStartingX = mStartingX; 362 mClampedStartingY = mStartingY; 363 } 364 } 365 366 /** 367 * Ends all animations, jumping values to the end state. 368 */ end()369 public void end() { 370 for (int i = 0; i < mRunningSwAnimators.size(); i++) { 371 mRunningSwAnimators.get(i).end(); 372 } 373 mRunningSwAnimators.clear(); 374 for (int i = 0; i < mRunningHwAnimators.size(); i++) { 375 mRunningHwAnimators.get(i).end(); 376 } 377 mRunningHwAnimators.clear(); 378 } 379 onAnimationPropertyChanged()380 private void onAnimationPropertyChanged() { 381 if (!mUsingProperties) { 382 invalidateSelf(); 383 } 384 } 385 clearHwProps()386 private void clearHwProps() { 387 mPropPaint = null; 388 mPropRadius = null; 389 mPropX = null; 390 mPropY = null; 391 mUsingProperties = false; 392 } 393 394 private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { 395 @Override 396 public void onAnimationEnd(Animator animator) { 397 mHasFinishedExit = true; 398 pruneHwFinished(); 399 pruneSwFinished(); 400 401 if (mRunningHwAnimators.isEmpty()) { 402 clearHwProps(); 403 } 404 } 405 }; 406 switchToUiThreadAnimation()407 private void switchToUiThreadAnimation() { 408 for (int i = 0; i < mRunningHwAnimators.size(); i++) { 409 Animator animator = mRunningHwAnimators.get(i); 410 animator.removeListener(mAnimationListener); 411 animator.end(); 412 } 413 mRunningHwAnimators.clear(); 414 clearHwProps(); 415 invalidateSelf(); 416 } 417 418 /** 419 * Property for animating radius between its initial and target values. 420 */ 421 private static final FloatProperty<RippleForeground> TWEEN_RADIUS = 422 new FloatProperty<RippleForeground>("tweenRadius") { 423 @Override 424 public void setValue(RippleForeground object, float value) { 425 object.mTweenRadius = value; 426 object.onAnimationPropertyChanged(); 427 } 428 429 @Override 430 public Float get(RippleForeground object) { 431 return object.mTweenRadius; 432 } 433 }; 434 435 /** 436 * Property for animating origin between its initial and target values. 437 */ 438 private static final FloatProperty<RippleForeground> TWEEN_ORIGIN = 439 new FloatProperty<RippleForeground>("tweenOrigin") { 440 @Override 441 public void setValue(RippleForeground object, float value) { 442 object.mTweenX = value; 443 object.mTweenY = value; 444 object.onAnimationPropertyChanged(); 445 } 446 447 @Override 448 public Float get(RippleForeground object) { 449 return object.mTweenX; 450 } 451 }; 452 453 /** 454 * Property for animating opacity between 0 and its target value. 455 */ 456 private static final FloatProperty<RippleForeground> OPACITY = 457 new FloatProperty<RippleForeground>("opacity") { 458 @Override 459 public void setValue(RippleForeground object, float value) { 460 object.mOpacity = value; 461 object.onAnimationPropertyChanged(); 462 } 463 464 @Override 465 public Float get(RippleForeground object) { 466 return object.mOpacity; 467 } 468 }; 469 } 470