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