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
17package android.graphics.drawable;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.graphics.Canvas;
24import android.graphics.CanvasProperty;
25import android.graphics.Paint;
26import android.graphics.Rect;
27import android.util.MathUtils;
28import android.view.HardwareCanvas;
29import android.view.RenderNodeAnimator;
30import android.view.animation.LinearInterpolator;
31
32import java.util.ArrayList;
33
34/**
35 * Draws a Material ripple.
36 */
37class 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     */
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
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
137    public boolean isHardwareAnimating() {
138        return mHardwareAnimating;
139    }
140
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
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
168    public void setOpacity(float a) {
169        mOpacity = a;
170        invalidateSelf();
171    }
172
173    public float getOpacity() {
174        return mOpacity;
175    }
176
177    @SuppressWarnings("unused")
178    public void setRadiusGravity(float r) {
179        mTweenRadius = r;
180        invalidateSelf();
181    }
182
183    @SuppressWarnings("unused")
184    public float getRadiusGravity() {
185        return mTweenRadius;
186    }
187
188    @SuppressWarnings("unused")
189    public void setXGravity(float x) {
190        mTweenX = x;
191        invalidateSelf();
192    }
193
194    @SuppressWarnings("unused")
195    public float getXGravity() {
196        return mTweenX;
197    }
198
199    @SuppressWarnings("unused")
200    public void setYGravity(float y) {
201        mTweenY = y;
202        invalidateSelf();
203    }
204
205    @SuppressWarnings("unused")
206    public float getYGravity() {
207        return mTweenY;
208    }
209
210    /**
211     * Draws the ripple centered at (0,0) using the specified paint.
212     */
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
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
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     */
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     */
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     */
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     */
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
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
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     */
419    public void jump() {
420        mCanceled = true;
421        endSoftwareAnimations();
422        cancelHardwareAnimations(true);
423        mCanceled = false;
424    }
425
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
448    private Paint getTempPaint(Paint original) {
449        if (mTempPaint == null) {
450            mTempPaint = new Paint();
451        }
452        mTempPaint.set(original);
453        return mTempPaint;
454    }
455
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     */
493    public void cancel() {
494        mCanceled = true;
495        cancelSoftwareAnimations();
496        cancelHardwareAnimations(false);
497        mCanceled = false;
498    }
499
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     */
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
552    private void removeSelf() {
553        // The owner will invalidate itself.
554        if (!mCanceled) {
555            mOwner.removeRipple(this);
556        }
557    }
558
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
575        public float getInterpolation(float input) {
576            return 1 - (float) Math.pow(400, -input * 1.4);
577        }
578    }
579}
580