RippleBackground.java revision a3f0c2b21a73a82a919abe247c4046d114f3712c
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.Color;
26import android.graphics.Paint;
27import android.graphics.Paint.Style;
28import android.graphics.Rect;
29import android.util.MathUtils;
30import android.view.HardwareCanvas;
31import android.view.RenderNodeAnimator;
32import android.view.animation.LinearInterpolator;
33
34import java.util.ArrayList;
35
36/**
37 * Draws a Material ripple.
38 */
39class RippleBackground {
40    private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
41
42    private static final float GLOBAL_SPEED = 1.0f;
43    private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
44    private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
45    private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED;
46    private static final float WAVE_OUTER_OPACITY_ENTER_VELOCITY = 10.0f * 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    // Hardware animators.
51    private final ArrayList<RenderNodeAnimator> mRunningAnimations =
52            new ArrayList<RenderNodeAnimator>();
53    private final ArrayList<RenderNodeAnimator> mPendingAnimations =
54            new ArrayList<RenderNodeAnimator>();
55
56    private final RippleDrawable mOwner;
57
58    /** Bounds used for computing max radius. */
59    private final Rect mBounds;
60
61    /** Full-opacity color for drawing this ripple. */
62    private int mColorOpaque;
63
64    /** Maximum alpha value for drawing this ripple. */
65    private int mColorAlpha;
66
67    /** Maximum ripple radius. */
68    private float mOuterRadius;
69
70    /** Screen density used to adjust pixel-based velocities. */
71    private float mDensity;
72
73    // Hardware rendering properties.
74    private CanvasProperty<Paint> mPropOuterPaint;
75    private CanvasProperty<Float> mPropOuterRadius;
76    private CanvasProperty<Float> mPropOuterX;
77    private CanvasProperty<Float> mPropOuterY;
78
79    // Software animators.
80    private ObjectAnimator mAnimOuterOpacity;
81
82    // Temporary paint used for creating canvas properties.
83    private Paint mTempPaint;
84
85    // Software rendering properties.
86    private float mOuterOpacity = 0;
87    private float mOuterX;
88    private float mOuterY;
89
90    /** Whether we should be drawing hardware animations. */
91    private boolean mHardwareAnimating;
92
93    /** Whether we can use hardware acceleration for the exit animation. */
94    private boolean mCanUseHardware;
95
96    /** Whether we have an explicit maximum radius. */
97    private boolean mHasMaxRadius;
98
99    /**
100     * Creates a new ripple.
101     */
102    public RippleBackground(RippleDrawable owner, Rect bounds) {
103        mOwner = owner;
104        mBounds = bounds;
105    }
106
107    public void setup(int maxRadius, int color, float density) {
108        mColorOpaque = color | 0xFF000000;
109        mColorAlpha = Color.alpha(color);
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    public boolean shouldDraw() {
166        final int outerAlpha = (int) (mColorAlpha * mOuterOpacity + 0.5f);
167        return mCanUseHardware && mHardwareAnimating || outerAlpha > 0 && mOuterRadius > 0;
168    }
169
170    private boolean drawHardware(HardwareCanvas c) {
171        // If we have any pending hardware animations, cancel any running
172        // animations and start those now.
173        final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
174        final int N = pendingAnimations.size();
175        if (N > 0) {
176            cancelHardwareAnimations(false);
177
178            // We canceled old animations, but we're about to run new ones.
179            mHardwareAnimating = true;
180
181            for (int i = 0; i < N; i++) {
182                pendingAnimations.get(i).setTarget(c);
183                pendingAnimations.get(i).start();
184            }
185
186            mRunningAnimations.addAll(pendingAnimations);
187            pendingAnimations.clear();
188        }
189
190        c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
191
192        return true;
193    }
194
195    private boolean drawSoftware(Canvas c, Paint p) {
196        boolean hasContent = false;
197
198        p.setColor(mColorOpaque);
199        final int outerAlpha = (int) (mColorAlpha * mOuterOpacity + 0.5f);
200        if (outerAlpha > 0 && mOuterRadius > 0) {
201            p.setAlpha(outerAlpha);
202            p.setStyle(Style.FILL);
203            c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
204            hasContent = true;
205        }
206
207        return hasContent;
208    }
209
210    /**
211     * Returns the maximum bounds of the ripple relative to the ripple center.
212     */
213    public void getBounds(Rect bounds) {
214        final int outerX = (int) mOuterX;
215        final int outerY = (int) mOuterY;
216        final int r = (int) mOuterRadius + 1;
217        bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
218    }
219
220    /**
221     * Starts the enter animation.
222     */
223    public void enter() {
224        cancel();
225
226        final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_ENTER_VELOCITY);
227        final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
228        outer.setAutoCancel(true);
229        outer.setDuration(outerDuration);
230        outer.setInterpolator(LINEAR_INTERPOLATOR);
231
232        mAnimOuterOpacity = outer;
233
234        // Enter animations always run on the UI thread, since it's unlikely
235        // that anything interesting is happening until the user lifts their
236        // finger.
237        outer.start();
238    }
239
240    /**
241     * Starts the exit animation.
242     */
243    public void exit() {
244        cancel();
245
246        // Scale the outer max opacity and opacity velocity based
247        // on the size of the outer radius.
248        final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
249        final float outerSizeInfluence = MathUtils.constrain(
250                (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
251                / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
252        final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN,
253                WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence);
254
255        // Determine at what time the inner and outer opacity intersect.
256        // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
257        // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
258        final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
259                / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
260        final int inflectionOpacity = (int) (mColorAlpha * (mOuterOpacity
261                + inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
262
263        if (mCanUseHardware) {
264            exitHardware(opacityDuration, inflectionDuration, inflectionOpacity);
265        } else {
266            exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity);
267        }
268    }
269
270    private void exitHardware(int opacityDuration, int inflectionDuration, int inflectionOpacity) {
271        mPendingAnimations.clear();
272
273        final Paint outerPaint = getTempPaint();
274        outerPaint.setAntiAlias(true);
275        outerPaint.setColor(mColorOpaque);
276        outerPaint.setAlpha((int) (mColorAlpha * mOuterOpacity + 0.5f));
277        outerPaint.setStyle(Style.FILL);
278        mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
279        mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
280        mPropOuterX = CanvasProperty.createFloat(mOuterX);
281        mPropOuterY = CanvasProperty.createFloat(mOuterY);
282
283        final RenderNodeAnimator outerOpacityAnim;
284        if (inflectionDuration > 0) {
285            // Outer opacity continues to increase for a bit.
286            outerOpacityAnim = new RenderNodeAnimator(mPropOuterPaint,
287                    RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
288            outerOpacityAnim.setDuration(inflectionDuration);
289            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
290
291            // Chain the outer opacity exit animation.
292            final int outerDuration = opacityDuration - inflectionDuration;
293            if (outerDuration > 0) {
294                final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
295                        mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
296                outerFadeOutAnim.setDuration(outerDuration);
297                outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
298                outerFadeOutAnim.setStartDelay(inflectionDuration);
299                outerFadeOutAnim.setStartValue(inflectionOpacity);
300                outerFadeOutAnim.addListener(mAnimationListener);
301
302                mPendingAnimations.add(outerFadeOutAnim);
303            } else {
304                outerOpacityAnim.addListener(mAnimationListener);
305            }
306        } else {
307            outerOpacityAnim = new RenderNodeAnimator(
308                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
309            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
310            outerOpacityAnim.setDuration(opacityDuration);
311            outerOpacityAnim.addListener(mAnimationListener);
312        }
313
314        mPendingAnimations.add(outerOpacityAnim);
315
316        mHardwareAnimating = true;
317
318        // Set up the software values to match the hardware end values.
319        mOuterOpacity = 0;
320
321        invalidateSelf();
322    }
323
324    /**
325     * Jump all animations to their end state. The caller is responsible for
326     * removing the ripple from the list of animating ripples.
327     */
328    public void jump() {
329        endSoftwareAnimations();
330        cancelHardwareAnimations(true);
331    }
332
333    private void endSoftwareAnimations() {
334        if (mAnimOuterOpacity != null) {
335            mAnimOuterOpacity.end();
336            mAnimOuterOpacity = null;
337        }
338    }
339
340    private Paint getTempPaint() {
341        if (mTempPaint == null) {
342            mTempPaint = new Paint();
343        }
344        return mTempPaint;
345    }
346
347    private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) {
348        final ObjectAnimator outerOpacityAnim;
349        if (inflectionDuration > 0) {
350            // Outer opacity continues to increase for a bit.
351            outerOpacityAnim = ObjectAnimator.ofFloat(this,
352                    "outerOpacity", inflectionOpacity / 255.0f);
353            outerOpacityAnim.setAutoCancel(true);
354            outerOpacityAnim.setDuration(inflectionDuration);
355            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
356
357            // Chain the outer opacity exit animation.
358            final int outerDuration = opacityDuration - inflectionDuration;
359            if (outerDuration > 0) {
360                outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
361                    @Override
362                    public void onAnimationEnd(Animator animation) {
363                        final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
364                                RippleBackground.this, "outerOpacity", 0);
365                        outerFadeOutAnim.setAutoCancel(true);
366                        outerFadeOutAnim.setDuration(outerDuration);
367                        outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
368                        outerFadeOutAnim.addListener(mAnimationListener);
369
370                        mAnimOuterOpacity = outerFadeOutAnim;
371
372                        outerFadeOutAnim.start();
373                    }
374
375                    @Override
376                    public void onAnimationCancel(Animator animation) {
377                        animation.removeListener(this);
378                    }
379                });
380            } else {
381                outerOpacityAnim.addListener(mAnimationListener);
382            }
383        } else {
384            outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
385            outerOpacityAnim.setAutoCancel(true);
386            outerOpacityAnim.setDuration(opacityDuration);
387            outerOpacityAnim.addListener(mAnimationListener);
388        }
389
390        mAnimOuterOpacity = outerOpacityAnim;
391
392        outerOpacityAnim.start();
393    }
394
395    /**
396     * Cancel all animations. The caller is responsible for removing
397     * the ripple from the list of animating ripples.
398     */
399    public void cancel() {
400        cancelSoftwareAnimations();
401        cancelHardwareAnimations(true);
402    }
403
404    private void cancelSoftwareAnimations() {
405        if (mAnimOuterOpacity != null) {
406            mAnimOuterOpacity.cancel();
407            mAnimOuterOpacity = null;
408        }
409    }
410
411    /**
412     * Cancels any running hardware animations.
413     */
414    private void cancelHardwareAnimations(boolean cancelPending) {
415        final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
416        final int N = runningAnimations.size();
417        for (int i = 0; i < N; i++) {
418            runningAnimations.get(i).cancel();
419        }
420        runningAnimations.clear();
421
422        if (cancelPending && !mPendingAnimations.isEmpty()) {
423            mPendingAnimations.clear();
424        }
425
426        mHardwareAnimating = false;
427    }
428
429    private void invalidateSelf() {
430        mOwner.invalidateSelf();
431    }
432
433    private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
434        @Override
435        public void onAnimationEnd(Animator animation) {
436            mHardwareAnimating = false;
437        }
438    };
439}
440