RippleBackground.java revision d78a44576c6bac5541e04c1f38599d43c9943653
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(true);
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(false);
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 + 1;
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    public void jump() {
407        endSoftwareAnimations();
408        endHardwareAnimations();
409    }
410
411    private void endSoftwareAnimations() {
412        if (mAnimOuterOpacity != null) {
413            mAnimOuterOpacity.end();
414        }
415
416        if (mAnimX != null) {
417            mAnimX.end();
418        }
419
420        if (mAnimY != null) {
421            mAnimY.end();
422        }
423    }
424
425    private void endHardwareAnimations() {
426        final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
427        final int N = runningAnimations.size();
428        for (int i = 0; i < N; i++) {
429            runningAnimations.get(i).end();
430        }
431        runningAnimations.clear();
432
433        // Abort any pending animations. Since we always have a completion
434        // listener on a pending animation, we also need to remove ourselves.
435        if (!mPendingAnimations.isEmpty()) {
436            mPendingAnimations.clear();
437            removeSelf();
438        }
439    }
440
441    private Paint getTempPaint() {
442        if (mTempPaint == null) {
443            mTempPaint = new Paint();
444        }
445        return mTempPaint;
446    }
447
448    private void exitSoftware(int opacityDuration, int outerInflection, int inflectionOpacity) {
449        final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
450        xAnim.setAutoCancel(true);
451        xAnim.setDuration(opacityDuration);
452        xAnim.setInterpolator(DECEL_INTERPOLATOR);
453
454        final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
455        yAnim.setAutoCancel(true);
456        yAnim.setDuration(opacityDuration);
457        yAnim.setInterpolator(DECEL_INTERPOLATOR);
458
459        final ObjectAnimator outerOpacityAnim;
460        if (outerInflection > 0) {
461            // Outer opacity continues to increase for a bit.
462            outerOpacityAnim = ObjectAnimator.ofFloat(this,
463                    "outerOpacity", inflectionOpacity / 255.0f);
464            outerOpacityAnim.setAutoCancel(true);
465            outerOpacityAnim.setDuration(outerInflection);
466            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
467
468            // Chain the outer opacity exit animation.
469            final int outerDuration = opacityDuration - outerInflection;
470            if (outerDuration > 0) {
471                outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
472                    @Override
473                    public void onAnimationEnd(Animator animation) {
474                        final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
475                                RippleBackground.this, "outerOpacity", 0);
476                        outerFadeOutAnim.setAutoCancel(true);
477                        outerFadeOutAnim.setDuration(outerDuration);
478                        outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
479                        outerFadeOutAnim.addListener(mAnimationListener);
480
481                        mAnimOuterOpacity = outerFadeOutAnim;
482
483                        outerFadeOutAnim.start();
484                    }
485
486                    @Override
487                    public void onAnimationCancel(Animator animation) {
488                        animation.removeListener(this);
489                    }
490                });
491            } else {
492                outerOpacityAnim.addListener(mAnimationListener);
493            }
494        } else {
495            outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
496            outerOpacityAnim.setAutoCancel(true);
497            outerOpacityAnim.setDuration(opacityDuration);
498            outerOpacityAnim.addListener(mAnimationListener);
499        }
500
501        mAnimOuterOpacity = outerOpacityAnim;
502        mAnimX = xAnim;
503        mAnimY = yAnim;
504
505        outerOpacityAnim.start();
506        xAnim.start();
507        yAnim.start();
508    }
509
510    /**
511     * Cancel all animations.
512     */
513    public void cancel() {
514        cancelSoftwareAnimations();
515        cancelHardwareAnimations(true);
516    }
517
518    private void cancelSoftwareAnimations() {
519        if (mAnimOuterOpacity != null) {
520            mAnimOuterOpacity.cancel();
521        }
522
523        if (mAnimX != null) {
524            mAnimX.cancel();
525        }
526
527        if (mAnimY != null) {
528            mAnimY.cancel();
529        }
530    }
531
532    /**
533     * Cancels any running hardware animations.
534     */
535    private void cancelHardwareAnimations(boolean cancelPending) {
536        final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
537        final int N = runningAnimations.size();
538        for (int i = 0; i < N; i++) {
539            runningAnimations.get(i).cancel();
540        }
541
542        runningAnimations.clear();
543
544        if (cancelPending && !mPendingAnimations.isEmpty()) {
545            mPendingAnimations.clear();
546            removeSelf();
547        }
548    }
549
550    private void removeSelf() {
551        // The owner will invalidate itself.
552        mOwner.removeBackground(this);
553    }
554
555    private void invalidateSelf() {
556        mOwner.invalidateSelf();
557    }
558
559    private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
560        @Override
561        public void onAnimationEnd(Animator animation) {
562            removeSelf();
563        }
564    };
565
566    /**
567    * Interpolator with a smooth log deceleration
568    */
569    private static final class LogInterpolator implements TimeInterpolator {
570        @Override
571        public float getInterpolation(float input) {
572            return 1 - (float) Math.pow(400, -input * 1.4);
573        }
574    }
575}
576