RippleBackground.java revision 6ce6d70f9c78f0197f1369246bf55a5f6b8d7ba4
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    /**
110     * Creates a new ripple.
111     */
112    public RippleBackground(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
113        mOwner = owner;
114        mBounds = bounds;
115
116        mStartingX = startingX;
117        mStartingY = startingY;
118    }
119
120    public void setup(int maxRadius, int color, float density) {
121        mColor = color | 0xFF000000;
122
123        if (maxRadius != RippleDrawable.RADIUS_AUTO) {
124            mHasMaxRadius = true;
125            mOuterRadius = maxRadius;
126        } else {
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        mOuterX = 0;
133        mOuterY = 0;
134        mDensity = density;
135
136        clampStartingPosition();
137    }
138
139    private void clampStartingPosition() {
140        final float cX = mBounds.exactCenterX();
141        final float cY = mBounds.exactCenterY();
142        final float dX = mStartingX - cX;
143        final float dY = mStartingY - cY;
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 = cX + (float) (Math.cos(angle) * r);
149            mClampedStartingY = cY + (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    @SuppressWarnings("unused")
167    public void setOuterOpacity(float a) {
168        mOuterOpacity = a;
169        invalidateSelf();
170    }
171
172    @SuppressWarnings("unused")
173    public float getOuterOpacity() {
174        return mOuterOpacity;
175    }
176
177    @SuppressWarnings("unused")
178    public void setXGravity(float x) {
179        mTweenX = x;
180        invalidateSelf();
181    }
182
183    @SuppressWarnings("unused")
184    public float getXGravity() {
185        return mTweenX;
186    }
187
188    @SuppressWarnings("unused")
189    public void setYGravity(float y) {
190        mTweenY = y;
191        invalidateSelf();
192    }
193
194    @SuppressWarnings("unused")
195    public float getYGravity() {
196        return mTweenY;
197    }
198
199    /**
200     * Draws the ripple centered at (0,0) using the specified paint.
201     */
202    public boolean draw(Canvas c, Paint p) {
203        final boolean canUseHardware = c.isHardwareAccelerated();
204        if (mCanUseHardware != canUseHardware && mCanUseHardware) {
205            // We've switched from hardware to non-hardware mode. Panic.
206            cancelHardwareAnimations();
207        }
208        mCanUseHardware = canUseHardware;
209
210        final boolean hasContent;
211        if (canUseHardware && mHardwareAnimating) {
212            hasContent = drawHardware((HardwareCanvas) c);
213        } else {
214            hasContent = drawSoftware(c, p);
215        }
216
217        return hasContent;
218    }
219
220    private boolean drawHardware(HardwareCanvas c) {
221        // If we have any pending hardware animations, cancel any running
222        // animations and start those now.
223        final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
224        final int N = pendingAnimations.size();
225        if (N > 0) {
226            cancelHardwareAnimations();
227
228            for (int i = 0; i < N; i++) {
229                pendingAnimations.get(i).setTarget(c);
230                pendingAnimations.get(i).start();
231            }
232
233            mRunningAnimations.addAll(pendingAnimations);
234            pendingAnimations.clear();
235        }
236
237        c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
238
239        return true;
240    }
241
242    private boolean drawSoftware(Canvas c, Paint p) {
243        boolean hasContent = false;
244
245        // Cache the paint alpha so we can restore it later.
246        final int paintAlpha = p.getAlpha();
247
248        final int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
249        if (outerAlpha > 0 && mOuterRadius > 0) {
250            p.setAlpha(outerAlpha);
251            p.setStyle(Style.FILL);
252            c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
253            hasContent = true;
254        }
255
256        p.setAlpha(paintAlpha);
257
258        return hasContent;
259    }
260
261    /**
262     * Returns the maximum bounds of the ripple relative to the ripple center.
263     */
264    public void getBounds(Rect bounds) {
265        final int outerX = (int) mOuterX;
266        final int outerY = (int) mOuterY;
267        final int r = (int) mOuterRadius;
268        bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
269    }
270
271    /**
272     * Specifies the starting position relative to the drawable bounds. No-op if
273     * the ripple has already entered.
274     */
275    public void move(float x, float y) {
276        mStartingX = x;
277        mStartingY = y;
278
279        clampStartingPosition();
280    }
281
282    /**
283     * Starts the enter animation.
284     */
285    public void enter() {
286        final int radiusDuration = (int)
287                (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
288        final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY_MIN);
289
290        final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
291        cX.setAutoCancel(true);
292        cX.setDuration(radiusDuration);
293        cX.setInterpolator(LINEAR_INTERPOLATOR);
294        cX.setStartDelay(RIPPLE_ENTER_DELAY);
295
296        final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
297        cY.setAutoCancel(true);
298        cY.setDuration(radiusDuration);
299        cY.setInterpolator(LINEAR_INTERPOLATOR);
300        cY.setStartDelay(RIPPLE_ENTER_DELAY);
301
302        final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
303        outer.setAutoCancel(true);
304        outer.setDuration(outerDuration);
305        outer.setInterpolator(LINEAR_INTERPOLATOR);
306
307        mAnimOuterOpacity = outer;
308        mAnimX = cX;
309        mAnimY = cY;
310
311        // Enter animations always run on the UI thread, since it's unlikely
312        // that anything interesting is happening until the user lifts their
313        // finger.
314        outer.start();
315        cX.start();
316        cY.start();
317    }
318
319    /**
320     * Starts the exit animation.
321     */
322    public void exit() {
323        cancelSoftwareAnimations();
324
325        // Scale the outer max opacity and opacity velocity based
326        // on the size of the outer radius.
327        final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
328        final float outerSizeInfluence = MathUtils.constrain(
329                (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
330                / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
331        final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_VELOCITY_MIN,
332                WAVE_OUTER_OPACITY_VELOCITY_MAX, outerSizeInfluence);
333
334        // Determine at what time the inner and outer opacity intersect.
335        // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
336        // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
337        final int outerInflection = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
338                / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
339        final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection
340                * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
341
342        if (mCanUseHardware) {
343            exitHardware(opacityDuration, outerInflection, inflectionOpacity);
344        } else {
345            exitSoftware(opacityDuration, outerInflection, inflectionOpacity);
346        }
347    }
348
349    private void exitHardware(int opacityDuration, int outerInflection, int inflectionOpacity) {
350        mPendingAnimations.clear();
351
352        // TODO: Adjust background by starting position.
353        final float startX = MathUtils.lerp(
354                mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
355        final float startY = MathUtils.lerp(
356                mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
357
358        final Paint outerPaint = getTempPaint();
359        outerPaint.setAntiAlias(true);
360        outerPaint.setColor(mColor);
361        outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f));
362        outerPaint.setStyle(Style.FILL);
363        mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
364        mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
365        mPropOuterX = CanvasProperty.createFloat(mOuterX);
366        mPropOuterY = CanvasProperty.createFloat(mOuterY);
367
368        final RenderNodeAnimator outerOpacityAnim;
369        if (outerInflection > 0) {
370            // Outer opacity continues to increase for a bit.
371            outerOpacityAnim = new RenderNodeAnimator(
372                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
373            outerOpacityAnim.setDuration(outerInflection);
374            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
375
376            // Chain the outer opacity exit animation.
377            final int outerDuration = opacityDuration - outerInflection;
378            if (outerDuration > 0) {
379                final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
380                        mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
381                outerFadeOutAnim.setDuration(outerDuration);
382                outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
383                outerFadeOutAnim.setStartDelay(outerInflection);
384                outerFadeOutAnim.setStartValue(inflectionOpacity);
385                outerFadeOutAnim.addListener(mAnimationListener);
386
387                mPendingAnimations.add(outerFadeOutAnim);
388            } else {
389                outerOpacityAnim.addListener(mAnimationListener);
390            }
391        } else {
392            outerOpacityAnim = new RenderNodeAnimator(
393                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
394            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
395            outerOpacityAnim.setDuration(opacityDuration);
396            outerOpacityAnim.addListener(mAnimationListener);
397        }
398
399        mPendingAnimations.add(outerOpacityAnim);
400
401        mHardwareAnimating = true;
402
403        invalidateSelf();
404    }
405
406    private Paint getTempPaint() {
407        if (mTempPaint == null) {
408            mTempPaint = new Paint();
409        }
410        return mTempPaint;
411    }
412
413    private void exitSoftware(int opacityDuration, int outerInflection, int inflectionOpacity) {
414        final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
415        xAnim.setAutoCancel(true);
416        xAnim.setDuration(opacityDuration);
417        xAnim.setInterpolator(DECEL_INTERPOLATOR);
418
419        final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
420        yAnim.setAutoCancel(true);
421        yAnim.setDuration(opacityDuration);
422        yAnim.setInterpolator(DECEL_INTERPOLATOR);
423
424        final ObjectAnimator outerOpacityAnim;
425        if (outerInflection > 0) {
426            // Outer opacity continues to increase for a bit.
427            outerOpacityAnim = ObjectAnimator.ofFloat(this,
428                    "outerOpacity", inflectionOpacity / 255.0f);
429            outerOpacityAnim.setAutoCancel(true);
430            outerOpacityAnim.setDuration(outerInflection);
431            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
432
433            // Chain the outer opacity exit animation.
434            final int outerDuration = opacityDuration - outerInflection;
435            if (outerDuration > 0) {
436                outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
437                    @Override
438                    public void onAnimationEnd(Animator animation) {
439                        final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
440                                RippleBackground.this, "outerOpacity", 0);
441                        outerFadeOutAnim.setAutoCancel(true);
442                        outerFadeOutAnim.setDuration(outerDuration);
443                        outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
444                        outerFadeOutAnim.addListener(mAnimationListener);
445
446                        mAnimOuterOpacity = outerFadeOutAnim;
447
448                        outerFadeOutAnim.start();
449                    }
450
451                    @Override
452                    public void onAnimationCancel(Animator animation) {
453                        animation.removeListener(this);
454                    }
455                });
456            } else {
457                outerOpacityAnim.addListener(mAnimationListener);
458            }
459        } else {
460            outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
461            outerOpacityAnim.setAutoCancel(true);
462            outerOpacityAnim.setDuration(opacityDuration);
463            outerOpacityAnim.addListener(mAnimationListener);
464        }
465
466        mAnimOuterOpacity = outerOpacityAnim;
467        mAnimX = xAnim;
468        mAnimY = yAnim;
469
470        outerOpacityAnim.start();
471        xAnim.start();
472        yAnim.start();
473    }
474
475    /**
476     * Cancel all animations.
477     */
478    public void cancel() {
479        cancelSoftwareAnimations();
480        cancelHardwareAnimations();
481    }
482
483    private void cancelSoftwareAnimations() {
484        if (mAnimOuterOpacity != null) {
485            mAnimOuterOpacity.cancel();
486        }
487
488        if (mAnimX != null) {
489            mAnimX.cancel();
490        }
491
492        if (mAnimY != null) {
493            mAnimY.cancel();
494        }
495    }
496
497    /**
498     * Cancels any running hardware animations.
499     */
500    private void cancelHardwareAnimations() {
501        final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
502        final int N = runningAnimations.size();
503        for (int i = 0; i < N; i++) {
504            runningAnimations.get(i).cancel();
505        }
506
507        runningAnimations.clear();
508    }
509
510    private void removeSelf() {
511        // The owner will invalidate itself.
512        mOwner.removeBackground(this);
513    }
514
515    private void invalidateSelf() {
516        mOwner.invalidateSelf();
517    }
518
519    private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
520        @Override
521        public void onAnimationEnd(Animator animation) {
522            removeSelf();
523        }
524    };
525
526    /**
527    * Interpolator with a smooth log deceleration
528    */
529    private static final class LogInterpolator implements TimeInterpolator {
530        @Override
531        public float getInterpolation(float input) {
532            return 1 - (float) Math.pow(400, -input * 1.4);
533        }
534    }
535}
536