RippleForeground.java revision f6829a0a618b4523619ec53c996b04d67e3186b9
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 17package android.graphics.drawable; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.animation.TimeInterpolator; 24import android.graphics.Canvas; 25import android.graphics.CanvasProperty; 26import android.graphics.Paint; 27import android.graphics.Rect; 28import android.util.FloatProperty; 29import android.util.MathUtils; 30import android.view.DisplayListCanvas; 31import android.view.RenderNodeAnimator; 32import android.view.animation.LinearInterpolator; 33 34/** 35 * Draws a ripple foreground. 36 */ 37class 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 private static final int RIPPLE_ENTER_DELAY = 80; 48 private static final int OPACITY_ENTER_DURATION_FAST = 120; 49 50 private float mStartingX; 51 private float mStartingY; 52 private float mClampedStartingX; 53 private float mClampedStartingY; 54 55 // Hardware rendering properties. 56 private CanvasProperty<Paint> mPropPaint; 57 private CanvasProperty<Float> mPropRadius; 58 private CanvasProperty<Float> mPropX; 59 private CanvasProperty<Float> mPropY; 60 61 // Software rendering properties. 62 private float mOpacity = 1; 63 private float mOuterX; 64 private float mOuterY; 65 66 // Values used to tween between the start and end positions. 67 private float mTweenRadius = 0; 68 private float mTweenX = 0; 69 private float mTweenY = 0; 70 71 /** Whether this ripple has finished its exit animation. */ 72 private boolean mHasFinishedExit; 73 74 public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY) { 75 super(owner, bounds); 76 77 mStartingX = startingX; 78 mStartingY = startingY; 79 } 80 81 @Override 82 public void onSetup() { 83 mOuterX = 0; 84 mOuterY = 0; 85 } 86 87 @Override 88 protected void onTargetRadiusChanged(float targetRadius) { 89 clampStartingPosition(); 90 } 91 92 @Override 93 protected boolean drawSoftware(Canvas c, Paint p) { 94 boolean hasContent = false; 95 96 final int origAlpha = p.getAlpha(); 97 final int alpha = (int) (origAlpha * mOpacity + 0.5f); 98 final float radius = MathUtils.lerp(0, mTargetRadius, mTweenRadius); 99 if (alpha > 0 && radius > 0) { 100 final float x = MathUtils.lerp( 101 mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); 102 final float y = MathUtils.lerp( 103 mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); 104 p.setAlpha(alpha); 105 c.drawCircle(x, y, radius, p); 106 p.setAlpha(origAlpha); 107 hasContent = true; 108 } 109 110 return hasContent; 111 } 112 113 @Override 114 protected boolean drawHardware(DisplayListCanvas c) { 115 c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint); 116 return true; 117 } 118 119 /** 120 * Returns the maximum bounds of the ripple relative to the ripple center. 121 */ 122 public void getBounds(Rect bounds) { 123 final int outerX = (int) mOuterX; 124 final int outerY = (int) mOuterY; 125 final int r = (int) mTargetRadius + 1; 126 bounds.set(outerX - r, outerY - r, outerX + r, outerY + r); 127 } 128 129 /** 130 * Specifies the starting position relative to the drawable bounds. No-op if 131 * the ripple has already entered. 132 */ 133 public void move(float x, float y) { 134 mStartingX = x; 135 mStartingY = y; 136 137 clampStartingPosition(); 138 } 139 140 /** 141 * @return {@code true} if this ripple has finished its exit animation 142 */ 143 public boolean hasFinishedExit() { 144 return mHasFinishedExit; 145 } 146 147 @Override 148 protected Animator createSoftwareEnter(boolean fast) { 149 final int duration = (int) 150 (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5); 151 152 final ObjectAnimator tweenAll = ObjectAnimator.ofFloat(this, TWEEN_ALL, 1); 153 tweenAll.setAutoCancel(true); 154 tweenAll.setDuration(duration); 155 tweenAll.setInterpolator(LINEAR_INTERPOLATOR); 156 tweenAll.setStartDelay(RIPPLE_ENTER_DELAY); 157 158 final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1); 159 opacity.setAutoCancel(true); 160 opacity.setDuration(OPACITY_ENTER_DURATION_FAST); 161 opacity.setInterpolator(LINEAR_INTERPOLATOR); 162 163 final AnimatorSet set = new AnimatorSet(); 164 set.play(tweenAll).with(opacity); 165 166 return set; 167 } 168 169 private int getRadiusExitDuration() { 170 final float radius = MathUtils.lerp(0, mTargetRadius, mTweenRadius); 171 final float remaining = mTargetRadius - radius; 172 return (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION 173 + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5); 174 } 175 176 private int getOpacityExitDuration() { 177 return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f); 178 } 179 180 @Override 181 protected Animator createSoftwareExit() { 182 final int radiusDuration = getRadiusExitDuration(); 183 final int opacityDuration = getOpacityExitDuration(); 184 185 final ObjectAnimator tweenAll = ObjectAnimator.ofFloat(this, TWEEN_ALL, 1); 186 tweenAll.setAutoCancel(true); 187 tweenAll.setDuration(radiusDuration); 188 tweenAll.setInterpolator(DECELERATE_INTERPOLATOR); 189 190 final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0); 191 opacity.setAutoCancel(true); 192 opacity.setDuration(opacityDuration); 193 opacity.setInterpolator(LINEAR_INTERPOLATOR); 194 195 final AnimatorSet set = new AnimatorSet(); 196 set.play(tweenAll).with(opacity); 197 set.addListener(mAnimationListener); 198 199 return set; 200 } 201 202 @Override 203 protected RenderNodeAnimatorSet createHardwareExit(Paint p) { 204 final int radiusDuration = getRadiusExitDuration(); 205 final int opacityDuration = getOpacityExitDuration(); 206 207 final float startX = MathUtils.lerp( 208 mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX); 209 final float startY = MathUtils.lerp( 210 mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY); 211 212 final float startRadius = MathUtils.lerp(0, mTargetRadius, mTweenRadius); 213 p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f)); 214 215 mPropPaint = CanvasProperty.createPaint(p); 216 mPropRadius = CanvasProperty.createFloat(startRadius); 217 mPropX = CanvasProperty.createFloat(startX); 218 mPropY = CanvasProperty.createFloat(startY); 219 220 final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius); 221 radius.setDuration(radiusDuration); 222 radius.setInterpolator(DECELERATE_INTERPOLATOR); 223 224 final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mOuterX); 225 x.setDuration(radiusDuration); 226 x.setInterpolator(DECELERATE_INTERPOLATOR); 227 228 final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mOuterY); 229 y.setDuration(radiusDuration); 230 y.setInterpolator(DECELERATE_INTERPOLATOR); 231 232 final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint, 233 RenderNodeAnimator.PAINT_ALPHA, 0); 234 opacity.setDuration(opacityDuration); 235 opacity.setInterpolator(LINEAR_INTERPOLATOR); 236 opacity.addListener(mAnimationListener); 237 238 final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet(); 239 set.add(radius); 240 set.add(opacity); 241 set.add(x); 242 set.add(y); 243 244 return set; 245 } 246 247 @Override 248 protected void jumpValuesToExit() { 249 mOpacity = 0; 250 mTweenX = 1; 251 mTweenY = 1; 252 mTweenRadius = 1; 253 } 254 255 /** 256 * Clamps the starting position to fit within the ripple bounds. 257 */ 258 private void clampStartingPosition() { 259 final float cX = mBounds.exactCenterX(); 260 final float cY = mBounds.exactCenterY(); 261 final float dX = mStartingX - cX; 262 final float dY = mStartingY - cY; 263 final float r = mTargetRadius; 264 if (dX * dX + dY * dY > r * r) { 265 // Point is outside the circle, clamp to the perimeter. 266 final double angle = Math.atan2(dY, dX); 267 mClampedStartingX = cX + (float) (Math.cos(angle) * r); 268 mClampedStartingY = cY + (float) (Math.sin(angle) * r); 269 } else { 270 mClampedStartingX = mStartingX; 271 mClampedStartingY = mStartingY; 272 } 273 } 274 275 private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() { 276 @Override 277 public void onAnimationEnd(Animator animator) { 278 mHasFinishedExit = true; 279 } 280 }; 281 282 /** 283 * Interpolator with a smooth log deceleration. 284 */ 285 private static final class LogDecelerateInterpolator implements TimeInterpolator { 286 private final float mBase; 287 private final float mDrift; 288 private final float mTimeScale; 289 private final float mOutputScale; 290 291 public LogDecelerateInterpolator(float base, float timeScale, float drift) { 292 mBase = base; 293 mDrift = drift; 294 mTimeScale = 1f / timeScale; 295 296 mOutputScale = 1f / computeLog(1f); 297 } 298 299 private float computeLog(float t) { 300 return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t); 301 } 302 303 @Override 304 public float getInterpolation(float t) { 305 return computeLog(t) * mOutputScale; 306 } 307 } 308 309 /** 310 * Property for animating radius, center X, and center Y between their 311 * initial and target values. 312 */ 313 private static final FloatProperty<RippleForeground> TWEEN_ALL = 314 new FloatProperty<RippleForeground>("tweenAll") { 315 @Override 316 public void setValue(RippleForeground object, float value) { 317 object.mTweenRadius = value; 318 object.mTweenX = value; 319 object.mTweenY = value; 320 object.invalidateSelf(); 321 } 322 323 @Override 324 public Float get(RippleForeground object) { 325 return object.mTweenRadius; 326 } 327 }; 328 329 /** 330 * Property for animating opacity between 0 and its target value. 331 */ 332 private static final FloatProperty<RippleForeground> OPACITY = 333 new FloatProperty<RippleForeground>("opacity") { 334 @Override 335 public void setValue(RippleForeground object, float value) { 336 object.mOpacity = value; 337 object.invalidateSelf(); 338 } 339 340 @Override 341 public Float get(RippleForeground object) { 342 return object.mOpacity; 343 } 344 }; 345} 346