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