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