RippleBackground.java revision a8a8ff000b2902eb4e187e62be39fd9535c6c839
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
41    private static final float GLOBAL_SPEED = 1.0f;
42    private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
43    private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
44    private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED;
45    private static final float WAVE_OUTER_OPACITY_ENTER_VELOCITY = 10.0f * GLOBAL_SPEED;
46    private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f;
47    private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f;
48
49    private static final long RIPPLE_ENTER_DELAY = 80;
50
51    // Hardware animators.
52    private final ArrayList<RenderNodeAnimator> mRunningAnimations =
53            new ArrayList<RenderNodeAnimator>();
54    private final ArrayList<RenderNodeAnimator> mPendingAnimations =
55            new ArrayList<RenderNodeAnimator>();
56
57    private final RippleDrawable mOwner;
58
59    /** Bounds used for computing max radius. */
60    private final Rect mBounds;
61
62    /** Full-opacity color for drawing this ripple. */
63    private int mColor;
64
65    /** Maximum ripple radius. */
66    private float mOuterRadius;
67
68    /** Screen density used to adjust pixel-based velocities. */
69    private float mDensity;
70
71    // Hardware rendering properties.
72    private CanvasProperty<Paint> mPropOuterPaint;
73    private CanvasProperty<Float> mPropOuterRadius;
74    private CanvasProperty<Float> mPropOuterX;
75    private CanvasProperty<Float> mPropOuterY;
76
77    // Software animators.
78    private ObjectAnimator mAnimOuterOpacity;
79
80    // Temporary paint used for creating canvas properties.
81    private Paint mTempPaint;
82
83    // Software rendering properties.
84    private float mOuterOpacity = 0;
85    private float mOuterX;
86    private float mOuterY;
87
88    /** Whether we should be drawing hardware animations. */
89    private boolean mHardwareAnimating;
90
91    /** Whether we can use hardware acceleration for the exit animation. */
92    private boolean mCanUseHardware;
93
94    /** Whether we have an explicit maximum radius. */
95    private boolean mHasMaxRadius;
96
97    /** Whether we were canceled externally and should avoid self-removal. */
98    private boolean mCanceled;
99
100    /**
101     * Creates a new ripple.
102     */
103    public RippleBackground(RippleDrawable owner, Rect bounds) {
104        mOwner = owner;
105        mBounds = bounds;
106    }
107
108    public void setup(int maxRadius, int color, float density) {
109        mColor = color | 0xFF000000;
110
111        if (maxRadius != RippleDrawable.RADIUS_AUTO) {
112            mHasMaxRadius = true;
113            mOuterRadius = maxRadius;
114        } else {
115            final float halfWidth = mBounds.width() / 2.0f;
116            final float halfHeight = mBounds.height() / 2.0f;
117            mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
118        }
119
120        mOuterX = 0;
121        mOuterY = 0;
122        mDensity = density;
123    }
124
125    public void onHotspotBoundsChanged() {
126        if (!mHasMaxRadius) {
127            final float halfWidth = mBounds.width() / 2.0f;
128            final float halfHeight = mBounds.height() / 2.0f;
129            mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
130        }
131    }
132
133    @SuppressWarnings("unused")
134    public void setOuterOpacity(float a) {
135        mOuterOpacity = a;
136        invalidateSelf();
137    }
138
139    @SuppressWarnings("unused")
140    public float getOuterOpacity() {
141        return mOuterOpacity;
142    }
143
144    /**
145     * Draws the ripple centered at (0,0) using the specified paint.
146     */
147    public boolean draw(Canvas c, Paint p) {
148        final boolean canUseHardware = c.isHardwareAccelerated();
149        if (mCanUseHardware != canUseHardware && mCanUseHardware) {
150            // We've switched from hardware to non-hardware mode. Panic.
151            cancelHardwareAnimations(true);
152        }
153        mCanUseHardware = canUseHardware;
154
155        final boolean hasContent;
156        if (canUseHardware && mHardwareAnimating) {
157            hasContent = drawHardware((HardwareCanvas) c);
158        } else {
159            hasContent = drawSoftware(c, p);
160        }
161
162        return hasContent;
163    }
164
165    private boolean drawHardware(HardwareCanvas c) {
166        // If we have any pending hardware animations, cancel any running
167        // animations and start those now.
168        final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
169        final int N = pendingAnimations.size();
170        if (N > 0) {
171            cancelHardwareAnimations(false);
172
173            for (int i = 0; i < N; i++) {
174                pendingAnimations.get(i).setTarget(c);
175                pendingAnimations.get(i).start();
176            }
177
178            mRunningAnimations.addAll(pendingAnimations);
179            pendingAnimations.clear();
180        }
181
182        c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
183
184        return true;
185    }
186
187    private boolean drawSoftware(Canvas c, Paint p) {
188        boolean hasContent = false;
189
190        // Cache the paint alpha so we can restore it later.
191        final int paintAlpha = p.getAlpha();
192
193        final int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
194        if (outerAlpha > 0 && mOuterRadius > 0) {
195            p.setAlpha(outerAlpha);
196            p.setStyle(Style.FILL);
197            c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
198            hasContent = true;
199        }
200
201        p.setAlpha(paintAlpha);
202
203        return hasContent;
204    }
205
206    /**
207     * Returns the maximum bounds of the ripple relative to the ripple center.
208     */
209    public void getBounds(Rect bounds) {
210        final int outerX = (int) mOuterX;
211        final int outerY = (int) mOuterY;
212        final int r = (int) mOuterRadius + 1;
213        bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
214    }
215
216    /**
217     * Starts the enter animation.
218     */
219    public void enter() {
220        cancel();
221
222        final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_ENTER_VELOCITY);
223        final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
224        outer.setAutoCancel(true);
225        outer.setDuration(outerDuration);
226        outer.setInterpolator(LINEAR_INTERPOLATOR);
227
228        mAnimOuterOpacity = outer;
229
230        // Enter animations always run on the UI thread, since it's unlikely
231        // that anything interesting is happening until the user lifts their
232        // finger.
233        outer.start();
234    }
235
236    /**
237     * Starts the exit animation.
238     */
239    public void exit() {
240        cancel();
241
242        // Scale the outer max opacity and opacity velocity based
243        // on the size of the outer radius.
244        final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
245        final float outerSizeInfluence = MathUtils.constrain(
246                (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
247                / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
248        final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN,
249                WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence);
250
251        // Determine at what time the inner and outer opacity intersect.
252        // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
253        // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
254        final int outerInflection = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
255                / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
256        final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection
257                * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
258
259        if (mCanUseHardware) {
260            exitHardware(opacityDuration, outerInflection, inflectionOpacity);
261        } else {
262            exitSoftware(opacityDuration, outerInflection, inflectionOpacity);
263        }
264    }
265
266    private void exitHardware(int opacityDuration, int outerInflection, int inflectionOpacity) {
267        mPendingAnimations.clear();
268
269        final Paint outerPaint = getTempPaint();
270        outerPaint.setAntiAlias(true);
271        outerPaint.setColor(mColor);
272        outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f));
273        outerPaint.setStyle(Style.FILL);
274        mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
275        mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
276        mPropOuterX = CanvasProperty.createFloat(mOuterX);
277        mPropOuterY = CanvasProperty.createFloat(mOuterY);
278
279        final RenderNodeAnimator outerOpacityAnim;
280        if (outerInflection > 0) {
281            // Outer opacity continues to increase for a bit.
282            outerOpacityAnim = new RenderNodeAnimator(
283                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
284            outerOpacityAnim.setDuration(outerInflection);
285            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
286
287            // Chain the outer opacity exit animation.
288            final int outerDuration = opacityDuration - outerInflection;
289            if (outerDuration > 0) {
290                final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
291                        mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
292                outerFadeOutAnim.setDuration(outerDuration);
293                outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
294                outerFadeOutAnim.setStartDelay(outerInflection);
295                outerFadeOutAnim.setStartValue(inflectionOpacity);
296                outerFadeOutAnim.addListener(mAnimationListener);
297
298                mPendingAnimations.add(outerFadeOutAnim);
299            } else {
300                outerOpacityAnim.addListener(mAnimationListener);
301            }
302        } else {
303            outerOpacityAnim = new RenderNodeAnimator(
304                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
305            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
306            outerOpacityAnim.setDuration(opacityDuration);
307            outerOpacityAnim.addListener(mAnimationListener);
308        }
309
310        mPendingAnimations.add(outerOpacityAnim);
311
312        mHardwareAnimating = true;
313
314        invalidateSelf();
315    }
316
317    /**
318     * Jump all animations to their end state. The caller is responsible for
319     * removing the ripple from the list of animating ripples.
320     */
321    public void jump() {
322        mCanceled = true;
323        endSoftwareAnimations();
324        endHardwareAnimations();
325        mCanceled = false;
326    }
327
328    private void endSoftwareAnimations() {
329        if (mAnimOuterOpacity != null) {
330            mAnimOuterOpacity.end();
331        }
332    }
333
334    private void endHardwareAnimations() {
335        final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
336        final int N = runningAnimations.size();
337        for (int i = 0; i < N; i++) {
338            runningAnimations.get(i).end();
339        }
340        runningAnimations.clear();
341
342        // Abort any pending animations. Since we always have a completion
343        // listener on a pending animation, we also need to remove ourselves.
344        if (!mPendingAnimations.isEmpty()) {
345            mPendingAnimations.clear();
346            removeSelf();
347        }
348
349        mHardwareAnimating = false;
350    }
351
352    private Paint getTempPaint() {
353        if (mTempPaint == null) {
354            mTempPaint = new Paint();
355        }
356        return mTempPaint;
357    }
358
359    private void exitSoftware(int opacityDuration, int outerInflection, int inflectionOpacity) {
360        final ObjectAnimator outerOpacityAnim;
361        if (outerInflection > 0) {
362            // Outer opacity continues to increase for a bit.
363            outerOpacityAnim = ObjectAnimator.ofFloat(this,
364                    "outerOpacity", inflectionOpacity / 255.0f);
365            outerOpacityAnim.setAutoCancel(true);
366            outerOpacityAnim.setDuration(outerInflection);
367            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
368
369            // Chain the outer opacity exit animation.
370            final int outerDuration = opacityDuration - outerInflection;
371            if (outerDuration > 0) {
372                outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
373                    @Override
374                    public void onAnimationEnd(Animator animation) {
375                        final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
376                                RippleBackground.this, "outerOpacity", 0);
377                        outerFadeOutAnim.setAutoCancel(true);
378                        outerFadeOutAnim.setDuration(outerDuration);
379                        outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
380                        outerFadeOutAnim.addListener(mAnimationListener);
381
382                        mAnimOuterOpacity = outerFadeOutAnim;
383
384                        outerFadeOutAnim.start();
385                    }
386
387                    @Override
388                    public void onAnimationCancel(Animator animation) {
389                        animation.removeListener(this);
390                    }
391                });
392            } else {
393                outerOpacityAnim.addListener(mAnimationListener);
394            }
395        } else {
396            outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
397            outerOpacityAnim.setAutoCancel(true);
398            outerOpacityAnim.setDuration(opacityDuration);
399            outerOpacityAnim.addListener(mAnimationListener);
400        }
401
402        mAnimOuterOpacity = outerOpacityAnim;
403
404        outerOpacityAnim.start();
405    }
406
407    /**
408     * Cancel all animations. The caller is responsible for removing
409     * the ripple from the list of animating ripples.
410     */
411    public void cancel() {
412        mCanceled = true;
413        cancelSoftwareAnimations();
414        cancelHardwareAnimations(true);
415        mCanceled = false;
416    }
417
418    private void cancelSoftwareAnimations() {
419        if (mAnimOuterOpacity != null) {
420            mAnimOuterOpacity.cancel();
421        }
422    }
423
424    /**
425     * Cancels any running hardware animations.
426     */
427    private void cancelHardwareAnimations(boolean cancelPending) {
428        final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
429        final int N = runningAnimations.size();
430        for (int i = 0; i < N; i++) {
431            runningAnimations.get(i).cancel();
432        }
433
434        runningAnimations.clear();
435
436        if (cancelPending && !mPendingAnimations.isEmpty()) {
437            mPendingAnimations.clear();
438        }
439
440        mHardwareAnimating = false;
441    }
442
443    private void removeSelf() {
444        // The owner will invalidate itself.
445        if (!mCanceled) {
446            mOwner.removeBackground(this);
447        }
448    }
449
450    private void invalidateSelf() {
451        mOwner.invalidateSelf();
452    }
453
454    private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
455        @Override
456        public void onAnimationEnd(Animator animation) {
457            removeSelf();
458        }
459    };
460
461    /**
462    * Interpolator with a smooth log deceleration
463    */
464    private static final class LogInterpolator implements TimeInterpolator {
465        @Override
466        public float getInterpolation(float input) {
467            return 1 - (float) Math.pow(400, -input * 1.4);
468        }
469    }
470}
471