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