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.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_SIZE_INFLUENCE_MAX = 200f;
46    private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f;
47
48    private static final int ENTER_DURATION = 667;
49    private static final int ENTER_DURATION_FAST = 100;
50
51    // Hardware animators.
52    private final ArrayList<RenderNodeAnimator> mRunningAnimations =
53            new ArrayList<RenderNodeAnimator>();
54
55    private final RippleDrawable mOwner;
56
57    /** Bounds used for computing max radius. */
58    private final Rect mBounds;
59
60    /** ARGB 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    // Hardware rendering properties.
70    private CanvasProperty<Paint> mPropOuterPaint;
71    private CanvasProperty<Float> mPropOuterRadius;
72    private CanvasProperty<Float> mPropOuterX;
73    private CanvasProperty<Float> mPropOuterY;
74
75    // Software animators.
76    private ObjectAnimator mAnimOuterOpacity;
77
78    // Temporary paint used for creating canvas properties.
79    private Paint mTempPaint;
80
81    // Software rendering properties.
82    private float mOuterOpacity = 0;
83    private float mOuterX;
84    private float mOuterY;
85
86    /** Whether we should be drawing hardware animations. */
87    private boolean mHardwareAnimating;
88
89    /** Whether we can use hardware acceleration for the exit animation. */
90    private boolean mCanUseHardware;
91
92    /** Whether we have an explicit maximum radius. */
93    private boolean mHasMaxRadius;
94
95    private boolean mHasPendingHardwareExit;
96    private int mPendingOpacityDuration;
97    private int mPendingInflectionDuration;
98    private int mPendingInflectionOpacity;
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, float density) {
109        if (maxRadius != RippleDrawable.RADIUS_AUTO) {
110            mHasMaxRadius = true;
111            mOuterRadius = maxRadius;
112        } else {
113            final float halfWidth = mBounds.width() / 2.0f;
114            final float halfHeight = mBounds.height() / 2.0f;
115            mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
116        }
117
118        mOuterX = 0;
119        mOuterY = 0;
120        mDensity = density;
121    }
122
123    public void onHotspotBoundsChanged() {
124        if (!mHasMaxRadius) {
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
131    @SuppressWarnings("unused")
132    public void setOuterOpacity(float a) {
133        mOuterOpacity = a;
134        invalidateSelf();
135    }
136
137    @SuppressWarnings("unused")
138    public float getOuterOpacity() {
139        return mOuterOpacity;
140    }
141
142    /**
143     * Draws the ripple centered at (0,0) using the specified paint.
144     */
145    public boolean draw(Canvas c, Paint p) {
146        mColor = p.getColor();
147
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 || mHasPendingHardwareExit)) {
157            hasContent = drawHardware((HardwareCanvas) c, p);
158        } else {
159            hasContent = drawSoftware(c, p);
160        }
161
162        return hasContent;
163    }
164
165    public boolean shouldDraw() {
166        return (mCanUseHardware && mHardwareAnimating) || (mOuterOpacity > 0 && mOuterRadius > 0);
167    }
168
169    private boolean drawHardware(HardwareCanvas c, Paint p) {
170        if (mHasPendingHardwareExit) {
171            cancelHardwareAnimations(false);
172            startPendingHardwareExit(c, p);
173        }
174
175        c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
176
177        return true;
178    }
179
180    private boolean drawSoftware(Canvas c, Paint p) {
181        boolean hasContent = false;
182
183        final int paintAlpha = p.getAlpha();
184        final int alpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
185        final float radius = mOuterRadius;
186        if (alpha > 0 && radius > 0) {
187            p.setAlpha(alpha);
188            c.drawCircle(mOuterX, mOuterY, radius, p);
189            p.setAlpha(paintAlpha);
190            hasContent = true;
191        }
192
193        return hasContent;
194    }
195
196    /**
197     * Returns the maximum bounds of the ripple relative to the ripple center.
198     */
199    public void getBounds(Rect bounds) {
200        final int outerX = (int) mOuterX;
201        final int outerY = (int) mOuterY;
202        final int r = (int) mOuterRadius + 1;
203        bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
204    }
205
206    /**
207     * Starts the enter animation.
208     */
209    public void enter(boolean fast) {
210        cancel();
211
212        final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
213        opacity.setAutoCancel(true);
214        opacity.setDuration(fast ? ENTER_DURATION_FAST : ENTER_DURATION);
215        opacity.setInterpolator(LINEAR_INTERPOLATOR);
216
217        mAnimOuterOpacity = opacity;
218
219        // Enter animations always run on the UI thread, since it's unlikely
220        // that anything interesting is happening until the user lifts their
221        // finger.
222        opacity.start();
223    }
224
225    /**
226     * Starts the exit animation.
227     */
228    public void exit() {
229        cancel();
230
231        // Scale the outer max opacity and opacity velocity based
232        // on the size of the outer radius.
233        final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
234        final float outerSizeInfluence = MathUtils.constrain(
235                (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
236                / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
237        final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN,
238                WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence);
239
240        // Determine at what time the inner and outer opacity intersect.
241        // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
242        // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
243        final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
244                / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
245        final int inflectionOpacity = (int) (Color.alpha(mColor) * (mOuterOpacity
246                + inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
247
248        if (mCanUseHardware) {
249            createPendingHardwareExit(opacityDuration, inflectionDuration, inflectionOpacity);
250        } else {
251            exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity);
252        }
253    }
254
255    private void createPendingHardwareExit(
256            int opacityDuration, int inflectionDuration, int inflectionOpacity) {
257        mHasPendingHardwareExit = true;
258        mPendingOpacityDuration = opacityDuration;
259        mPendingInflectionDuration = inflectionDuration;
260        mPendingInflectionOpacity = inflectionOpacity;
261
262        // The animation will start on the next draw().
263        invalidateSelf();
264    }
265
266    private void startPendingHardwareExit(HardwareCanvas c, Paint p) {
267        mHasPendingHardwareExit = false;
268
269        final int opacityDuration = mPendingOpacityDuration;
270        final int inflectionDuration = mPendingInflectionDuration;
271        final int inflectionOpacity = mPendingInflectionOpacity;
272
273        final Paint outerPaint = getTempPaint(p);
274        outerPaint.setAlpha((int) (outerPaint.getAlpha() * mOuterOpacity + 0.5f));
275        mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
276        mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
277        mPropOuterX = CanvasProperty.createFloat(mOuterX);
278        mPropOuterY = CanvasProperty.createFloat(mOuterY);
279
280        final RenderNodeAnimator outerOpacityAnim;
281        if (inflectionDuration > 0) {
282            // Outer opacity continues to increase for a bit.
283            outerOpacityAnim = new RenderNodeAnimator(mPropOuterPaint,
284                    RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
285            outerOpacityAnim.setDuration(inflectionDuration);
286            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
287
288            // Chain the outer opacity exit animation.
289            final int outerDuration = opacityDuration - inflectionDuration;
290            if (outerDuration > 0) {
291                final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
292                        mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
293                outerFadeOutAnim.setDuration(outerDuration);
294                outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
295                outerFadeOutAnim.setStartDelay(inflectionDuration);
296                outerFadeOutAnim.setStartValue(inflectionOpacity);
297                outerFadeOutAnim.addListener(mAnimationListener);
298                outerFadeOutAnim.setTarget(c);
299                outerFadeOutAnim.start();
300
301                mRunningAnimations.add(outerFadeOutAnim);
302            } else {
303                outerOpacityAnim.addListener(mAnimationListener);
304            }
305        } else {
306            outerOpacityAnim = new RenderNodeAnimator(
307                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
308            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
309            outerOpacityAnim.setDuration(opacityDuration);
310            outerOpacityAnim.addListener(mAnimationListener);
311        }
312
313        outerOpacityAnim.setTarget(c);
314        outerOpacityAnim.start();
315
316        mRunningAnimations.add(outerOpacityAnim);
317
318        mHardwareAnimating = true;
319
320        // Set up the software values to match the hardware end values.
321        mOuterOpacity = 0;
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(Paint original) {
341        if (mTempPaint == null) {
342            mTempPaint = new Paint();
343        }
344        mTempPaint.set(original);
345        return mTempPaint;
346    }
347
348    private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) {
349        final ObjectAnimator outerOpacityAnim;
350        if (inflectionDuration > 0) {
351            // Outer opacity continues to increase for a bit.
352            outerOpacityAnim = ObjectAnimator.ofFloat(this,
353                    "outerOpacity", inflectionOpacity / 255.0f);
354            outerOpacityAnim.setAutoCancel(true);
355            outerOpacityAnim.setDuration(inflectionDuration);
356            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
357
358            // Chain the outer opacity exit animation.
359            final int outerDuration = opacityDuration - inflectionDuration;
360            if (outerDuration > 0) {
361                outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
362                    @Override
363                    public void onAnimationEnd(Animator animation) {
364                        final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
365                                RippleBackground.this, "outerOpacity", 0);
366                        outerFadeOutAnim.setAutoCancel(true);
367                        outerFadeOutAnim.setDuration(outerDuration);
368                        outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
369                        outerFadeOutAnim.addListener(mAnimationListener);
370
371                        mAnimOuterOpacity = outerFadeOutAnim;
372
373                        outerFadeOutAnim.start();
374                    }
375
376                    @Override
377                    public void onAnimationCancel(Animator animation) {
378                        animation.removeListener(this);
379                    }
380                });
381            } else {
382                outerOpacityAnim.addListener(mAnimationListener);
383            }
384        } else {
385            outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
386            outerOpacityAnim.setAutoCancel(true);
387            outerOpacityAnim.setDuration(opacityDuration);
388            outerOpacityAnim.addListener(mAnimationListener);
389        }
390
391        mAnimOuterOpacity = outerOpacityAnim;
392
393        outerOpacityAnim.start();
394    }
395
396    /**
397     * Cancel all animations. The caller is responsible for removing
398     * the ripple from the list of animating ripples.
399     */
400    public void cancel() {
401        cancelSoftwareAnimations();
402        cancelHardwareAnimations(false);
403    }
404
405    private void cancelSoftwareAnimations() {
406        if (mAnimOuterOpacity != null) {
407            mAnimOuterOpacity.cancel();
408            mAnimOuterOpacity = null;
409        }
410    }
411
412    /**
413     * Cancels any running hardware animations.
414     */
415    private void cancelHardwareAnimations(boolean jumpToEnd) {
416        final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
417        final int N = runningAnimations.size();
418        for (int i = 0; i < N; i++) {
419            if (jumpToEnd) {
420                runningAnimations.get(i).end();
421            } else {
422                runningAnimations.get(i).cancel();
423            }
424        }
425        runningAnimations.clear();
426
427        if (mHasPendingHardwareExit) {
428            // If we had a pending hardware exit, jump to the end state.
429            mHasPendingHardwareExit = false;
430
431            if (jumpToEnd) {
432                mOuterOpacity = 0;
433            }
434        }
435
436        mHardwareAnimating = false;
437    }
438
439    private void invalidateSelf() {
440        mOwner.invalidateSelf();
441    }
442
443    private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
444        @Override
445        public void onAnimationEnd(Animator animation) {
446            mHardwareAnimating = false;
447        }
448    };
449}
450