1 /* 2 * Copyright (C) 2020 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.TimeInterpolator; 21 import android.animation.ValueAnimator; 22 import android.annotation.ColorInt; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.graphics.Canvas; 26 import android.graphics.CanvasProperty; 27 import android.graphics.Paint; 28 import android.graphics.RecordingCanvas; 29 import android.graphics.animation.RenderNodeAnimator; 30 import android.view.animation.AnimationUtils; 31 import android.view.animation.Interpolator; 32 import android.view.animation.LinearInterpolator; 33 import android.view.animation.PathInterpolator; 34 35 import java.util.function.Consumer; 36 37 /** 38 * @hide 39 */ 40 public final class RippleAnimationSession { 41 private static final String TAG = "RippleAnimationSession"; 42 private static final int ENTER_ANIM_DURATION = 450; 43 private static final int EXIT_ANIM_DURATION = 375; 44 private static final long NOISE_ANIMATION_DURATION = 7000; 45 private static final long MAX_NOISE_PHASE = NOISE_ANIMATION_DURATION / 214; 46 private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 47 private static final Interpolator FAST_OUT_SLOW_IN = 48 new PathInterpolator(0.4f, 0f, 0.2f, 1f); 49 private Consumer<RippleAnimationSession> mOnSessionEnd; 50 private final AnimationProperties<Float, Paint> mProperties; 51 private AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> mCanvasProperties; 52 private Runnable mOnUpdate; 53 private long mStartTime; 54 private boolean mForceSoftware; 55 private Animator mLoopAnimation; 56 private Animator mCurrentAnimation; 57 RippleAnimationSession(@onNull AnimationProperties<Float, Paint> properties, boolean forceSoftware)58 RippleAnimationSession(@NonNull AnimationProperties<Float, Paint> properties, 59 boolean forceSoftware) { 60 mProperties = properties; 61 mForceSoftware = forceSoftware; 62 } 63 isForceSoftware()64 boolean isForceSoftware() { 65 return mForceSoftware; 66 } 67 enter(Canvas canvas)68 @NonNull RippleAnimationSession enter(Canvas canvas) { 69 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 70 if (useRTAnimations(canvas)) { 71 enterHardware((RecordingCanvas) canvas); 72 } else { 73 enterSoftware(); 74 } 75 return this; 76 } 77 end()78 void end() { 79 if (mCurrentAnimation != null) { 80 mCurrentAnimation.end(); 81 } 82 } 83 exit(Canvas canvas)84 @NonNull RippleAnimationSession exit(Canvas canvas) { 85 if (useRTAnimations(canvas)) exitHardware((RecordingCanvas) canvas); 86 else exitSoftware(); 87 return this; 88 } 89 onAnimationEnd(Animator anim)90 private void onAnimationEnd(Animator anim) { 91 notifyUpdate(); 92 } 93 setOnSessionEnd( @ullable Consumer<RippleAnimationSession> onSessionEnd)94 @NonNull RippleAnimationSession setOnSessionEnd( 95 @Nullable Consumer<RippleAnimationSession> onSessionEnd) { 96 mOnSessionEnd = onSessionEnd; 97 return this; 98 } 99 setOnAnimationUpdated(@ullable Runnable run)100 RippleAnimationSession setOnAnimationUpdated(@Nullable Runnable run) { 101 mOnUpdate = run; 102 return this; 103 } 104 useRTAnimations(Canvas canvas)105 private boolean useRTAnimations(Canvas canvas) { 106 if (mForceSoftware) return false; 107 if (!canvas.isHardwareAccelerated()) return false; 108 RecordingCanvas hwCanvas = (RecordingCanvas) canvas; 109 if (hwCanvas.mNode == null || !hwCanvas.mNode.isAttached()) return false; 110 return true; 111 } 112 exitSoftware()113 private void exitSoftware() { 114 ValueAnimator expand = ValueAnimator.ofFloat(.5f, 1f); 115 expand.setDuration(EXIT_ANIM_DURATION); 116 expand.setStartDelay(computeDelay()); 117 expand.addUpdateListener(updatedAnimation -> { 118 notifyUpdate(); 119 mProperties.getShader().setProgress((Float) expand.getAnimatedValue()); 120 }); 121 expand.addListener(new AnimatorListener(this) { 122 @Override 123 public void onAnimationEnd(Animator animation) { 124 super.onAnimationEnd(animation); 125 if (mLoopAnimation != null) mLoopAnimation.cancel(); 126 Consumer<RippleAnimationSession> onEnd = mOnSessionEnd; 127 if (onEnd != null) onEnd.accept(RippleAnimationSession.this); 128 if (mCurrentAnimation == expand) mCurrentAnimation = null; 129 } 130 }); 131 expand.setInterpolator(LINEAR_INTERPOLATOR); 132 expand.start(); 133 mCurrentAnimation = expand; 134 } 135 computeDelay()136 private long computeDelay() { 137 final long timePassed = AnimationUtils.currentAnimationTimeMillis() - mStartTime; 138 return Math.max((long) ENTER_ANIM_DURATION - timePassed, 0); 139 } 140 notifyUpdate()141 private void notifyUpdate() { 142 if (mOnUpdate != null) mOnUpdate.run(); 143 } 144 setForceSoftwareAnimation(boolean forceSw)145 RippleAnimationSession setForceSoftwareAnimation(boolean forceSw) { 146 mForceSoftware = forceSw; 147 return this; 148 } 149 exitHardware(RecordingCanvas canvas)150 private void exitHardware(RecordingCanvas canvas) { 151 AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> 152 props = getCanvasProperties(); 153 RenderNodeAnimator exit = 154 new RenderNodeAnimator(props.getProgress(), 1f); 155 exit.setDuration(EXIT_ANIM_DURATION); 156 exit.addListener(new AnimatorListener(this) { 157 @Override 158 public void onAnimationEnd(Animator animation) { 159 super.onAnimationEnd(animation); 160 if (mLoopAnimation != null) mLoopAnimation.cancel(); 161 Consumer<RippleAnimationSession> onEnd = mOnSessionEnd; 162 if (onEnd != null) onEnd.accept(RippleAnimationSession.this); 163 if (mCurrentAnimation == exit) mCurrentAnimation = null; 164 } 165 }); 166 exit.setTarget(canvas); 167 exit.setInterpolator(LINEAR_INTERPOLATOR); 168 169 long delay = computeDelay(); 170 exit.setStartDelay(delay); 171 exit.start(); 172 mCurrentAnimation = exit; 173 } 174 enterHardware(RecordingCanvas canvas)175 private void enterHardware(RecordingCanvas canvas) { 176 AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> 177 props = getCanvasProperties(); 178 RenderNodeAnimator expand = 179 new RenderNodeAnimator(props.getProgress(), .5f); 180 expand.setTarget(canvas); 181 RenderNodeAnimator loop = new RenderNodeAnimator(props.getNoisePhase(), 182 mStartTime + MAX_NOISE_PHASE); 183 loop.setTarget(canvas); 184 startAnimation(expand, loop); 185 mCurrentAnimation = expand; 186 } 187 startAnimation(Animator expand, Animator loop)188 private void startAnimation(Animator expand, Animator loop) { 189 expand.setDuration(ENTER_ANIM_DURATION); 190 expand.addListener(new AnimatorListener(this)); 191 expand.setInterpolator(FAST_OUT_SLOW_IN); 192 expand.start(); 193 loop.setDuration(NOISE_ANIMATION_DURATION); 194 loop.addListener(new AnimatorListener(this) { 195 @Override 196 public void onAnimationEnd(Animator animation) { 197 super.onAnimationEnd(animation); 198 mLoopAnimation = null; 199 } 200 }); 201 loop.setInterpolator(LINEAR_INTERPOLATOR); 202 loop.start(); 203 if (mLoopAnimation != null) mLoopAnimation.cancel(); 204 mLoopAnimation = loop; 205 } 206 enterSoftware()207 private void enterSoftware() { 208 ValueAnimator expand = ValueAnimator.ofFloat(0f, 0.5f); 209 expand.addUpdateListener(updatedAnimation -> { 210 notifyUpdate(); 211 mProperties.getShader().setProgress((float) expand.getAnimatedValue()); 212 }); 213 ValueAnimator loop = ValueAnimator.ofFloat(mStartTime, mStartTime + MAX_NOISE_PHASE); 214 loop.addUpdateListener(updatedAnimation -> { 215 notifyUpdate(); 216 mProperties.getShader().setNoisePhase((float) loop.getAnimatedValue()); 217 }); 218 startAnimation(expand, loop); 219 mCurrentAnimation = expand; 220 } 221 setRadius(float radius)222 void setRadius(float radius) { 223 mProperties.setRadius(radius); 224 mProperties.getShader().setRadius(radius); 225 if (mCanvasProperties != null) { 226 mCanvasProperties.setRadius(CanvasProperty.createFloat(radius)); 227 mCanvasProperties.getShader().setRadius(radius); 228 } 229 } 230 getProperties()231 @NonNull AnimationProperties<Float, Paint> getProperties() { 232 return mProperties; 233 } 234 235 @NonNull getCanvasProperties()236 AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> getCanvasProperties() { 237 if (mCanvasProperties == null) { 238 mCanvasProperties = new AnimationProperties<>( 239 CanvasProperty.createFloat(mProperties.getX()), 240 CanvasProperty.createFloat(mProperties.getY()), 241 CanvasProperty.createFloat(mProperties.getMaxRadius()), 242 CanvasProperty.createFloat(mProperties.getNoisePhase()), 243 CanvasProperty.createPaint(mProperties.getPaint()), 244 CanvasProperty.createFloat(mProperties.getProgress()), 245 mProperties.getColor(), 246 mProperties.getShader()); 247 } 248 return mCanvasProperties; 249 } 250 251 private static class AnimatorListener implements Animator.AnimatorListener { 252 private final RippleAnimationSession mSession; 253 AnimatorListener(RippleAnimationSession session)254 AnimatorListener(RippleAnimationSession session) { 255 mSession = session; 256 } 257 258 @Override onAnimationStart(Animator animation)259 public void onAnimationStart(Animator animation) { 260 261 } 262 263 @Override onAnimationEnd(Animator animation)264 public void onAnimationEnd(Animator animation) { 265 mSession.onAnimationEnd(animation); 266 } 267 268 @Override onAnimationCancel(Animator animation)269 public void onAnimationCancel(Animator animation) { 270 271 } 272 273 @Override onAnimationRepeat(Animator animation)274 public void onAnimationRepeat(Animator animation) { 275 276 } 277 } 278 279 static class AnimationProperties<FloatType, PaintType> { 280 private final FloatType mProgress; 281 private FloatType mMaxRadius; 282 private final FloatType mNoisePhase; 283 private final PaintType mPaint; 284 private final RippleShader mShader; 285 private final @ColorInt int mColor; 286 private FloatType mX; 287 private FloatType mY; 288 AnimationProperties(FloatType x, FloatType y, FloatType maxRadius, FloatType noisePhase, PaintType paint, FloatType progress, @ColorInt int color, RippleShader shader)289 AnimationProperties(FloatType x, FloatType y, FloatType maxRadius, FloatType noisePhase, 290 PaintType paint, FloatType progress, @ColorInt int color, RippleShader shader) { 291 mY = y; 292 mX = x; 293 mMaxRadius = maxRadius; 294 mNoisePhase = noisePhase; 295 mPaint = paint; 296 mShader = shader; 297 mProgress = progress; 298 mColor = color; 299 } 300 getProgress()301 FloatType getProgress() { 302 return mProgress; 303 } 304 setRadius(FloatType radius)305 void setRadius(FloatType radius) { 306 mMaxRadius = radius; 307 } 308 setOrigin(FloatType x, FloatType y)309 void setOrigin(FloatType x, FloatType y) { 310 mX = x; 311 mY = y; 312 } 313 getX()314 FloatType getX() { 315 return mX; 316 } 317 getY()318 FloatType getY() { 319 return mY; 320 } 321 getMaxRadius()322 FloatType getMaxRadius() { 323 return mMaxRadius; 324 } 325 getPaint()326 PaintType getPaint() { 327 return mPaint; 328 } 329 getShader()330 RippleShader getShader() { 331 return mShader; 332 } 333 getNoisePhase()334 FloatType getNoisePhase() { 335 return mNoisePhase; 336 } 337 getColor()338 @ColorInt int getColor() { 339 return mColor; 340 } 341 } 342 } 343