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.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.TimeInterpolator; 24 import android.graphics.Canvas; 25 import android.graphics.CanvasProperty; 26 import android.graphics.Paint; 27 import android.graphics.Rect; 28 import android.util.FloatProperty; 29 import android.util.MathUtils; 30 import android.view.DisplayListCanvas; 31 import android.view.RenderNodeAnimator; 32 import android.view.animation.LinearInterpolator; 33 34 /** 35 * Draws a ripple foreground. 36 */ 37 class RippleForeground extends RippleComponent { 38 private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 39 private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator( 40 400f, 1.4f, 0); 41 42 // Pixel-based accelerations and velocities. 43 private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024; 44 private static final float WAVE_TOUCH_UP_ACCELERATION = 3400; 45 private static final float WAVE_OPACITY_DECAY_VELOCITY = 3; 46 47 // Bounded ripple animation properties. 48 private static final int BOUNDED_ORIGIN_EXIT_DURATION = 300; 49 private static final int BOUNDED_RADIUS_EXIT_DURATION = 800; 50 private static final int BOUNDED_OPACITY_EXIT_DURATION = 400; 51 private static final float MAX_BOUNDED_RADIUS = 350; 52 53 private static final int RIPPLE_ENTER_DELAY = 80; 54 private static final int OPACITY_ENTER_DURATION_FAST = 120; 55 56 // Parent-relative values for starting position. 57 private float mStartingX; 58 private float mStartingY; 59 private float mClampedStartingX; 60 private float mClampedStartingY; 61 62 // Hardware rendering properties. 63 private CanvasProperty<Paint> mPropPaint; 64 private CanvasProperty<Float> mPropRadius; 65 private CanvasProperty<Float> mPropX; 66 private CanvasProperty<Float> mPropY; 67 68 // Target values for tween animations. 69 private float mTargetX = 0; 70 private float mTargetY = 0; 71 72 /** Ripple target radius used when bounded. Not used for clamping. */ 73 private float mBoundedRadius = 0; 74 75 // Software rendering properties. 76 private float mOpacity = 1; 77 78 // Values used to tween between the start and end positions. 79 private float mTweenRadius = 0; 80 private float mTweenX = 0; 81 private float mTweenY = 0; 82 83 /** Whether this ripple is bounded. */ 84 private boolean mIsBounded; 85 86 /** Whether this ripple has finished its exit animation. */ 87 private boolean mHasFinishedExit; 88 RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY, boolean isBounded, boolean forceSoftware)89 public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY, 90 boolean isBounded, boolean forceSoftware) { 91 super(owner, bounds, forceSoftware); 92 93 mIsBounded = isBounded; 94 mStartingX = startingX; 95 mStartingY = startingY; 96 97 if (isBounded) { 98 mBoundedRadius = MAX_BOUNDED_RADIUS * 0.9f 99 + (float) (MAX_BOUNDED_RADIUS * Math.random() * 0.1); 100 } else { 101 mBoundedRadius = 0; 102 } 103 } 104 105 @Override onTargetRadiusChanged(float targetRadius)106 protected void onTargetRadiusChanged(float targetRadius) { 107 clampStartingPosition(); 108 } 109 110 @Override drawSoftware(Canvas c, Paint p)111 protected boolean drawSoftware(Canvas c, Paint p) { 112 boolean hasContent = false; 113 114 final int origAlpha = p.getAlpha(); 115 final int alpha = (int) (origAlpha * mOpacity + 0.5f); 116 final float radius = getCurrentRadius(); 117 if (alpha > 0 && radius > 0) { 118 final float x = getCurrentX(); 119 final float y = getCurrentY(); 120 p.setAlpha(alpha); 121 c.drawCircle(x, y, radius, p); 122 p.setAlpha(origAlpha); 123 hasContent = true; 124 } 125 126 return hasContent; 127 } 128 129 @Override drawHardware(DisplayListCanvas c)130 protected boolean drawHardware(DisplayListCanvas c) { 131 c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); 132 return true; 133 } 134 135 /** 136 * Returns the maximum bounds of the ripple relative to the ripple center. 137 */ getBounds(Rect bounds)138 public void getBounds(Rect bounds) { 139 final int outerX = (int) mTargetX; 140 final int outerY = (int) mTargetY; 141 final int r = (int) mTargetRadius + 1; 142 bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); 143 } 144 145 /** 146 * Specifies the starting position relative to the drawable bounds. No-op if 147 * the ripple has already entered. 148 */ move(float x, float y)149 public void move(float x, float y) { 150 mStartingX = x; 151 mStartingY = y; 152 153 clampStartingPosition(); 154 } 155 156 /** 157 * @return {@code true} if this ripple has finished its exit animation 158 */ hasFinishedExit()159 public boolean hasFinishedExit() { 160 return mHasFinishedExit; 161 } 162 163 @Override createSoftwareEnter(boolean fast)164 protected Animator createSoftwareEnter(boolean fast) { 165 // Bounded ripples don't have enter animations. 166 if (mIsBounded) { 167 return null; 168 } 169 170 final int duration = (int) 171 (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensityScale) + 0.5); 172 173 final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); 174 tweenRadius.setAutoCancel(true); 175 tweenRadius.setDuration(duration); 176 tweenRadius.setInterpolator(LINEAR_INTERPOLATOR); 177 tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY); 178 179 final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); 180 tweenOrigin.setAutoCancel(true); 181 tweenOrigin.setDuration(duration); 182 tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR); 183 tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY); 184 185 final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); 186 opacity.setAutoCancel(true); 187 opacity.setDuration(OPACITY_ENTER_DURATION_FAST); 188 opacity.setInterpolator(LINEAR_INTERPOLATOR); 189 190 final AnimatorSet set = new AnimatorSet(); 191 set.play(tweenOrigin).with(tweenRadius).with(opacity); 192 193 return set; 194 } 195 getCurrentX()196 private float getCurrentX() { 197 return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX); 198 } 199 getCurrentY()200 private float getCurrentY() { 201 return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY); 202 } 203 getRadiusExitDuration()204 private int getRadiusExitDuration() { 205 final float remainingRadius = mTargetRadius - getCurrentRadius(); 206 return (int) (1000 * Math.sqrt(remainingRadius / (WAVE_TOUCH_UP_ACCELERATION 207 + WAVE_TOUCH_DOWN_ACCELERATION) * mDensityScale) + 0.5); 208 } 209 getCurrentRadius()210 private float getCurrentRadius() { 211 return MathUtils.lerp(0, mTargetRadius, mTweenRadius); 212 } 213 getOpacityExitDuration()214 private int getOpacityExitDuration() { 215 return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); 216 } 217 218 /** 219 * Compute target values that are dependent on bounding. 220 */ computeBoundedTargetValues()221 private void computeBoundedTargetValues() { 222 mTargetX = (mClampedStartingX - mBounds.exactCenterX()) * .7f; 223 mTargetY = (mClampedStartingY - mBounds.exactCenterY()) * .7f; 224 mTargetRadius = mBoundedRadius; 225 } 226 227 @Override createSoftwareExit()228 protected Animator createSoftwareExit() { 229 final int radiusDuration; 230 final int originDuration; 231 final int opacityDuration; 232 if (mIsBounded) { 233 computeBoundedTargetValues(); 234 235 radiusDuration = BOUNDED_RADIUS_EXIT_DURATION; 236 originDuration = BOUNDED_ORIGIN_EXIT_DURATION; 237 opacityDuration = BOUNDED_OPACITY_EXIT_DURATION; 238 } else { 239 radiusDuration = getRadiusExitDuration(); 240 originDuration = radiusDuration; 241 opacityDuration = getOpacityExitDuration(); 242 } 243 244 final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1); 245 tweenRadius.setAutoCancel(true); 246 tweenRadius.setDuration(radiusDuration); 247 tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR); 248 249 final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1); 250 tweenOrigin.setAutoCancel(true); 251 tweenOrigin.setDuration(originDuration); 252 tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR); 253 254 final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0); 255 opacity.setAutoCancel(true); 256 opacity.setDuration(opacityDuration); 257 opacity.setInterpolator(LINEAR_INTERPOLATOR); 258 259 final AnimatorSet set = new AnimatorSet(); 260 set.play(tweenOrigin).with(tweenRadius).with(opacity); 261 set.addListener(mAnimationListener); 262 263 return set; 264 } 265 266 @Override createHardwareExit(Paint p)267 protected RenderNodeAnimatorSet createHardwareExit(Paint p) { 268 final int radiusDuration; 269 final int originDuration; 270 final int opacityDuration; 271 if (mIsBounded) { 272 computeBoundedTargetValues(); 273 274 radiusDuration = BOUNDED_RADIUS_EXIT_DURATION; 275 originDuration = BOUNDED_ORIGIN_EXIT_DURATION; 276 opacityDuration = BOUNDED_OPACITY_EXIT_DURATION; 277 } else { 278 radiusDuration = getRadiusExitDuration(); 279 originDuration = radiusDuration; 280 opacityDuration = getOpacityExitDuration(); 281 } 282 283 final float startX = getCurrentX(); 284 final float startY = getCurrentY(); 285 final float startRadius = getCurrentRadius(); 286 287 p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f)); 288 289 mPropPaint = CanvasProperty.createPaint(p); 290 mPropRadius = CanvasProperty.createFloat(startRadius); 291 mPropX = CanvasProperty.createFloat(startX); 292 mPropY = CanvasProperty.createFloat(startY); 293 294 final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius); 295 radius.setDuration(radiusDuration); 296 radius.setInterpolator(DECELERATE_INTERPOLATOR); 297 298 final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX); 299 x.setDuration(originDuration); 300 x.setInterpolator(DECELERATE_INTERPOLATOR); 301 302 final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY); 303 y.setDuration(originDuration); 304 y.setInterpolator(DECELERATE_INTERPOLATOR); 305 306 final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, 307 RenderNodeAnimator.PAINT_ALPHA, 0); 308 opacity.setDuration(opacityDuration); 309 opacity.setInterpolator(LINEAR_INTERPOLATOR); 310 opacity.addListener(mAnimationListener); 311 312 final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); 313 set.add(radius); 314 set.add(opacity); 315 set.add(x); 316 set.add(y); 317 318 return set; 319 } 320 321 @Override jumpValuesToExit()322 protected void jumpValuesToExit() { 323 mOpacity = 0; 324 mTweenX = 1; 325 mTweenY = 1; 326 mTweenRadius = 1; 327 } 328 329 /** 330 * Clamps the starting position to fit within the ripple bounds. 331 */ clampStartingPosition()332 private void clampStartingPosition() { 333 final float cX = mBounds.exactCenterX(); 334 final float cY = mBounds.exactCenterY(); 335 final float dX = mStartingX - cX; 336 final float dY = mStartingY - cY; 337 final float r = mTargetRadius; 338 if (dX * dX + dY * dY > r * r) { 339 // Point is outside the circle, clamp to the perimeter. 340 final double angle = Math.atan2(dY, dX); 341 mClampedStartingX = cX + (float) (Math.cos(angle) * r); 342 mClampedStartingY = cY + (float) (Math.sin(angle) * r); 343 } else { 344 mClampedStartingX = mStartingX; 345 mClampedStartingY = mStartingY; 346 } 347 } 348 349 private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { 350 @Override 351 public void onAnimationEnd(Animator animator) { 352 mHasFinishedExit = true; 353 } 354 }; 355 356 /** 357 * Interpolator with a smooth log deceleration. 358 */ 359 private static final class LogDecelerateInterpolator implements TimeInterpolator { 360 private final float mBase; 361 private final float mDrift; 362 private final float mTimeScale; 363 private final float mOutputScale; 364 LogDecelerateInterpolator(float base, float timeScale, float drift)365 public LogDecelerateInterpolator(float base, float timeScale, float drift) { 366 mBase = base; 367 mDrift = drift; 368 mTimeScale = 1f / timeScale; 369 370 mOutputScale = 1f / computeLog(1f); 371 } 372 computeLog(float t)373 private float computeLog(float t) { 374 return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t); 375 } 376 377 @Override getInterpolation(float t)378 public float getInterpolation(float t) { 379 return computeLog(t) * mOutputScale; 380 } 381 } 382 383 /** 384 * Property for animating radius between its initial and target values. 385 */ 386 private static final FloatProperty<RippleForeground> TWEEN_RADIUS = 387 new FloatProperty<RippleForeground>("tweenRadius") { 388 @Override 389 public void setValue(RippleForeground object, float value) { 390 object.mTweenRadius = value; 391 object.invalidateSelf(); 392 } 393 394 @Override 395 public Float get(RippleForeground object) { 396 return object.mTweenRadius; 397 } 398 }; 399 400 /** 401 * Property for animating origin between its initial and target values. 402 */ 403 private static final FloatProperty<RippleForeground> TWEEN_ORIGIN = 404 new FloatProperty<RippleForeground>("tweenOrigin") { 405 @Override 406 public void setValue(RippleForeground object, float value) { 407 object.mTweenX = value; 408 object.mTweenY = value; 409 object.invalidateSelf(); 410 } 411 412 @Override 413 public Float get(RippleForeground object) { 414 return object.mTweenX; 415 } 416 }; 417 418 /** 419 * Property for animating opacity between 0 and its target value. 420 */ 421 private static final FloatProperty<RippleForeground> OPACITY = 422 new FloatProperty<RippleForeground>("opacity") { 423 @Override 424 public void setValue(RippleForeground object, float value) { 425 object.mOpacity = value; 426 object.invalidateSelf(); 427 } 428 429 @Override 430 public Float get(RippleForeground object) { 431 return object.mOpacity; 432 } 433 }; 434 } 435