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