1 /*
2  * Copyright (C) 2013 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.Rect;
27 import android.util.MathUtils;
28 import android.view.HardwareCanvas;
29 import android.view.RenderNodeAnimator;
30 import android.view.animation.LinearInterpolator;
31 
32 import java.util.ArrayList;
33 
34 /**
35  * Draws a Material ripple.
36  */
37 class Ripple {
38     private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
39     private static final TimeInterpolator DECEL_INTERPOLATOR = new LogInterpolator();
40 
41     private static final float GLOBAL_SPEED = 1.0f;
42     private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED;
43     private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED;
44     private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
45 
46     private static final long RIPPLE_ENTER_DELAY = 80;
47 
48     // Hardware animators.
49     private final ArrayList<RenderNodeAnimator> mRunningAnimations =
50             new ArrayList<RenderNodeAnimator>();
51 
52     private final RippleDrawable mOwner;
53 
54     /** Bounds used for computing max radius. */
55     private final Rect mBounds;
56 
57     /** Maximum ripple radius. */
58     private float mOuterRadius;
59 
60     /** Screen density used to adjust pixel-based velocities. */
61     private float mDensity;
62 
63     private float mStartingX;
64     private float mStartingY;
65     private float mClampedStartingX;
66     private float mClampedStartingY;
67 
68     // Hardware rendering properties.
69     private CanvasProperty<Paint> mPropPaint;
70     private CanvasProperty<Float> mPropRadius;
71     private CanvasProperty<Float> mPropX;
72     private CanvasProperty<Float> mPropY;
73 
74     // Software animators.
75     private ObjectAnimator mAnimRadius;
76     private ObjectAnimator mAnimOpacity;
77     private ObjectAnimator mAnimX;
78     private ObjectAnimator mAnimY;
79 
80     // Temporary paint used for creating canvas properties.
81     private Paint mTempPaint;
82 
83     // Software rendering properties.
84     private float mOpacity = 1;
85     private float mOuterX;
86     private float mOuterY;
87 
88     // Values used to tween between the start and end positions.
89     private float mTweenRadius = 0;
90     private float mTweenX = 0;
91     private float mTweenY = 0;
92 
93     /** Whether we should be drawing hardware animations. */
94     private boolean mHardwareAnimating;
95 
96     /** Whether we can use hardware acceleration for the exit animation. */
97     private boolean mCanUseHardware;
98 
99     /** Whether we have an explicit maximum radius. */
100     private boolean mHasMaxRadius;
101 
102     /** Whether we were canceled externally and should avoid self-removal. */
103     private boolean mCanceled;
104 
105     private boolean mHasPendingHardwareExit;
106     private int mPendingRadiusDuration;
107     private int mPendingOpacityDuration;
108 
109     /**
110      * Creates a new ripple.
111      */
Ripple(RippleDrawable owner, Rect bounds, float startingX, float startingY)112     public Ripple(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
113         mOwner = owner;
114         mBounds = bounds;
115 
116         mStartingX = startingX;
117         mStartingY = startingY;
118     }
119 
setup(int maxRadius, float density)120     public void setup(int maxRadius, float density) {
121         if (maxRadius != RippleDrawable.RADIUS_AUTO) {
122             mHasMaxRadius = true;
123             mOuterRadius = maxRadius;
124         } else {
125             final float halfWidth = mBounds.width() / 2.0f;
126             final float halfHeight = mBounds.height() / 2.0f;
127             mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
128         }
129 
130         mOuterX = 0;
131         mOuterY = 0;
132         mDensity = density;
133 
134         clampStartingPosition();
135     }
136 
isHardwareAnimating()137     public boolean isHardwareAnimating() {
138         return mHardwareAnimating;
139     }
140 
clampStartingPosition()141     private void clampStartingPosition() {
142         final float cX = mBounds.exactCenterX();
143         final float cY = mBounds.exactCenterY();
144         final float dX = mStartingX - cX;
145         final float dY = mStartingY - cY;
146         final float r = mOuterRadius;
147         if (dX * dX + dY * dY > r * r) {
148             // Point is outside the circle, clamp to the circumference.
149             final double angle = Math.atan2(dY, dX);
150             mClampedStartingX = cX + (float) (Math.cos(angle) * r);
151             mClampedStartingY = cY + (float) (Math.sin(angle) * r);
152         } else {
153             mClampedStartingX = mStartingX;
154             mClampedStartingY = mStartingY;
155         }
156     }
157 
onHotspotBoundsChanged()158     public void onHotspotBoundsChanged() {
159         if (!mHasMaxRadius) {
160             final float halfWidth = mBounds.width() / 2.0f;
161             final float halfHeight = mBounds.height() / 2.0f;
162             mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
163 
164             clampStartingPosition();
165         }
166     }
167 
setOpacity(float a)168     public void setOpacity(float a) {
169         mOpacity = a;
170         invalidateSelf();
171     }
172 
getOpacity()173     public float getOpacity() {
174         return mOpacity;
175     }
176 
177     @SuppressWarnings("unused")
setRadiusGravity(float r)178     public void setRadiusGravity(float r) {
179         mTweenRadius = r;
180         invalidateSelf();
181     }
182 
183     @SuppressWarnings("unused")
getRadiusGravity()184     public float getRadiusGravity() {
185         return mTweenRadius;
186     }
187 
188     @SuppressWarnings("unused")
setXGravity(float x)189     public void setXGravity(float x) {
190         mTweenX = x;
191         invalidateSelf();
192     }
193 
194     @SuppressWarnings("unused")
getXGravity()195     public float getXGravity() {
196         return mTweenX;
197     }
198 
199     @SuppressWarnings("unused")
setYGravity(float y)200     public void setYGravity(float y) {
201         mTweenY = y;
202         invalidateSelf();
203     }
204 
205     @SuppressWarnings("unused")
getYGravity()206     public float getYGravity() {
207         return mTweenY;
208     }
209 
210     /**
211      * Draws the ripple centered at (0,0) using the specified paint.
212      */
draw(Canvas c, Paint p)213     public boolean draw(Canvas c, Paint p) {
214         final boolean canUseHardware = c.isHardwareAccelerated();
215         if (mCanUseHardware != canUseHardware && mCanUseHardware) {
216             // We've switched from hardware to non-hardware mode. Panic.
217             cancelHardwareAnimations(true);
218         }
219         mCanUseHardware = canUseHardware;
220 
221         final boolean hasContent;
222         if (canUseHardware && (mHardwareAnimating || mHasPendingHardwareExit)) {
223             hasContent = drawHardware((HardwareCanvas) c, p);
224         } else {
225             hasContent = drawSoftware(c, p);
226         }
227 
228         return hasContent;
229     }
230 
drawHardware(HardwareCanvas c, Paint p)231     private boolean drawHardware(HardwareCanvas c, Paint p) {
232         if (mHasPendingHardwareExit) {
233             cancelHardwareAnimations(false);
234             startPendingHardwareExit(c, p);
235         }
236 
237         c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
238 
239         return true;
240     }
241 
drawSoftware(Canvas c, Paint p)242     private boolean drawSoftware(Canvas c, Paint p) {
243         boolean hasContent = false;
244 
245         final int paintAlpha = p.getAlpha();
246         final int alpha = (int) (paintAlpha * mOpacity + 0.5f);
247         final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
248         if (alpha > 0 && radius > 0) {
249             final float x = MathUtils.lerp(
250                     mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
251             final float y = MathUtils.lerp(
252                     mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
253             p.setAlpha(alpha);
254             c.drawCircle(x, y, radius, p);
255             p.setAlpha(paintAlpha);
256             hasContent = true;
257         }
258 
259         return hasContent;
260     }
261 
262     /**
263      * Returns the maximum bounds of the ripple relative to the ripple center.
264      */
getBounds(Rect bounds)265     public void getBounds(Rect bounds) {
266         final int outerX = (int) mOuterX;
267         final int outerY = (int) mOuterY;
268         final int r = (int) mOuterRadius + 1;
269         bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
270     }
271 
272     /**
273      * Specifies the starting position relative to the drawable bounds. No-op if
274      * the ripple has already entered.
275      */
move(float x, float y)276     public void move(float x, float y) {
277         mStartingX = x;
278         mStartingY = y;
279 
280         clampStartingPosition();
281     }
282 
283     /**
284      * Starts the enter animation.
285      */
enter()286     public void enter() {
287         cancel();
288 
289         final int radiusDuration = (int)
290                 (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
291 
292         final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
293         radius.setAutoCancel(true);
294         radius.setDuration(radiusDuration);
295         radius.setInterpolator(LINEAR_INTERPOLATOR);
296         radius.setStartDelay(RIPPLE_ENTER_DELAY);
297 
298         final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
299         cX.setAutoCancel(true);
300         cX.setDuration(radiusDuration);
301         cX.setInterpolator(LINEAR_INTERPOLATOR);
302         cX.setStartDelay(RIPPLE_ENTER_DELAY);
303 
304         final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
305         cY.setAutoCancel(true);
306         cY.setDuration(radiusDuration);
307         cY.setInterpolator(LINEAR_INTERPOLATOR);
308         cY.setStartDelay(RIPPLE_ENTER_DELAY);
309 
310         mAnimRadius = radius;
311         mAnimX = cX;
312         mAnimY = cY;
313 
314         // Enter animations always run on the UI thread, since it's unlikely
315         // that anything interesting is happening until the user lifts their
316         // finger.
317         radius.start();
318         cX.start();
319         cY.start();
320     }
321 
322     /**
323      * Starts the exit animation.
324      */
exit()325     public void exit() {
326         final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
327         final float remaining;
328         if (mAnimRadius != null && mAnimRadius.isRunning()) {
329             remaining = mOuterRadius - radius;
330         } else {
331             remaining = mOuterRadius;
332         }
333 
334         cancel();
335 
336         final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
337                 + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
338         final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
339 
340         if (mCanUseHardware) {
341             createPendingHardwareExit(radiusDuration, opacityDuration);
342         } else {
343             exitSoftware(radiusDuration, opacityDuration);
344         }
345     }
346 
createPendingHardwareExit(int radiusDuration, int opacityDuration)347     private void createPendingHardwareExit(int radiusDuration, int opacityDuration) {
348         mHasPendingHardwareExit = true;
349         mPendingRadiusDuration = radiusDuration;
350         mPendingOpacityDuration = opacityDuration;
351 
352         // The animation will start on the next draw().
353         invalidateSelf();
354     }
355 
startPendingHardwareExit(HardwareCanvas c, Paint p)356     private void startPendingHardwareExit(HardwareCanvas c, Paint p) {
357         mHasPendingHardwareExit = false;
358 
359         final int radiusDuration = mPendingRadiusDuration;
360         final int opacityDuration = mPendingOpacityDuration;
361 
362         final float startX = MathUtils.lerp(
363                 mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
364         final float startY = MathUtils.lerp(
365                 mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
366 
367         final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
368         final Paint paint = getTempPaint(p);
369         paint.setAlpha((int) (paint.getAlpha() * mOpacity + 0.5f));
370         mPropPaint = CanvasProperty.createPaint(paint);
371         mPropRadius = CanvasProperty.createFloat(startRadius);
372         mPropX = CanvasProperty.createFloat(startX);
373         mPropY = CanvasProperty.createFloat(startY);
374 
375         final RenderNodeAnimator radiusAnim = new RenderNodeAnimator(mPropRadius, mOuterRadius);
376         radiusAnim.setDuration(radiusDuration);
377         radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
378         radiusAnim.setTarget(c);
379         radiusAnim.start();
380 
381         final RenderNodeAnimator xAnim = new RenderNodeAnimator(mPropX, mOuterX);
382         xAnim.setDuration(radiusDuration);
383         xAnim.setInterpolator(DECEL_INTERPOLATOR);
384         xAnim.setTarget(c);
385         xAnim.start();
386 
387         final RenderNodeAnimator yAnim = new RenderNodeAnimator(mPropY, mOuterY);
388         yAnim.setDuration(radiusDuration);
389         yAnim.setInterpolator(DECEL_INTERPOLATOR);
390         yAnim.setTarget(c);
391         yAnim.start();
392 
393         final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPropPaint,
394                 RenderNodeAnimator.PAINT_ALPHA, 0);
395         opacityAnim.setDuration(opacityDuration);
396         opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
397         opacityAnim.addListener(mAnimationListener);
398         opacityAnim.setTarget(c);
399         opacityAnim.start();
400 
401         mRunningAnimations.add(radiusAnim);
402         mRunningAnimations.add(opacityAnim);
403         mRunningAnimations.add(xAnim);
404         mRunningAnimations.add(yAnim);
405 
406         mHardwareAnimating = true;
407 
408         // Set up the software values to match the hardware end values.
409         mOpacity = 0;
410         mTweenX = 1;
411         mTweenY = 1;
412         mTweenRadius = 1;
413     }
414 
415     /**
416      * Jump all animations to their end state. The caller is responsible for
417      * removing the ripple from the list of animating ripples.
418      */
jump()419     public void jump() {
420         mCanceled = true;
421         endSoftwareAnimations();
422         cancelHardwareAnimations(true);
423         mCanceled = false;
424     }
425 
endSoftwareAnimations()426     private void endSoftwareAnimations() {
427         if (mAnimRadius != null) {
428             mAnimRadius.end();
429             mAnimRadius = null;
430         }
431 
432         if (mAnimOpacity != null) {
433             mAnimOpacity.end();
434             mAnimOpacity = null;
435         }
436 
437         if (mAnimX != null) {
438             mAnimX.end();
439             mAnimX = null;
440         }
441 
442         if (mAnimY != null) {
443             mAnimY.end();
444             mAnimY = null;
445         }
446     }
447 
getTempPaint(Paint original)448     private Paint getTempPaint(Paint original) {
449         if (mTempPaint == null) {
450             mTempPaint = new Paint();
451         }
452         mTempPaint.set(original);
453         return mTempPaint;
454     }
455 
exitSoftware(int radiusDuration, int opacityDuration)456     private void exitSoftware(int radiusDuration, int opacityDuration) {
457         final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
458         radiusAnim.setAutoCancel(true);
459         radiusAnim.setDuration(radiusDuration);
460         radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
461 
462         final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
463         xAnim.setAutoCancel(true);
464         xAnim.setDuration(radiusDuration);
465         xAnim.setInterpolator(DECEL_INTERPOLATOR);
466 
467         final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
468         yAnim.setAutoCancel(true);
469         yAnim.setDuration(radiusDuration);
470         yAnim.setInterpolator(DECEL_INTERPOLATOR);
471 
472         final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0);
473         opacityAnim.setAutoCancel(true);
474         opacityAnim.setDuration(opacityDuration);
475         opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
476         opacityAnim.addListener(mAnimationListener);
477 
478         mAnimRadius = radiusAnim;
479         mAnimOpacity = opacityAnim;
480         mAnimX = xAnim;
481         mAnimY = yAnim;
482 
483         radiusAnim.start();
484         opacityAnim.start();
485         xAnim.start();
486         yAnim.start();
487     }
488 
489     /**
490      * Cancels all animations. The caller is responsible for removing
491      * the ripple from the list of animating ripples.
492      */
cancel()493     public void cancel() {
494         mCanceled = true;
495         cancelSoftwareAnimations();
496         cancelHardwareAnimations(false);
497         mCanceled = false;
498     }
499 
cancelSoftwareAnimations()500     private void cancelSoftwareAnimations() {
501         if (mAnimRadius != null) {
502             mAnimRadius.cancel();
503             mAnimRadius = null;
504         }
505 
506         if (mAnimOpacity != null) {
507             mAnimOpacity.cancel();
508             mAnimOpacity = null;
509         }
510 
511         if (mAnimX != null) {
512             mAnimX.cancel();
513             mAnimX = null;
514         }
515 
516         if (mAnimY != null) {
517             mAnimY.cancel();
518             mAnimY = null;
519         }
520     }
521 
522     /**
523      * Cancels any running hardware animations.
524      */
cancelHardwareAnimations(boolean jumpToEnd)525     private void cancelHardwareAnimations(boolean jumpToEnd) {
526         final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
527         final int N = runningAnimations.size();
528         for (int i = 0; i < N; i++) {
529             if (jumpToEnd) {
530                 runningAnimations.get(i).end();
531             } else {
532                 runningAnimations.get(i).cancel();
533             }
534         }
535         runningAnimations.clear();
536 
537         if (mHasPendingHardwareExit) {
538             // If we had a pending hardware exit, jump to the end state.
539             mHasPendingHardwareExit = false;
540 
541             if (jumpToEnd) {
542                 mOpacity = 0;
543                 mTweenX = 1;
544                 mTweenY = 1;
545                 mTweenRadius = 1;
546             }
547         }
548 
549         mHardwareAnimating = false;
550     }
551 
removeSelf()552     private void removeSelf() {
553         // The owner will invalidate itself.
554         if (!mCanceled) {
555             mOwner.removeRipple(this);
556         }
557     }
558 
invalidateSelf()559     private void invalidateSelf() {
560         mOwner.invalidateSelf();
561     }
562 
563     private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
564         @Override
565         public void onAnimationEnd(Animator animation) {
566             removeSelf();
567         }
568     };
569 
570     /**
571     * Interpolator with a smooth log deceleration
572     */
573     private static final class LogInterpolator implements TimeInterpolator {
574         @Override
getInterpolation(float input)575         public float getInterpolation(float input) {
576             return 1 - (float) Math.pow(400, -input * 1.4);
577         }
578     }
579 }
580