Ripple.java revision dc6046fca37264b48e9c959f38d08cfb78f436ed
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 Quantum Paper ripple.
37 */
38class Ripple {
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_TOUCH_DOWN_ACCELERATION = 512.0f * GLOBAL_SPEED;
43    private static final float WAVE_TOUCH_UP_ACCELERATION = 1024.0f * GLOBAL_SPEED;
44    private static final float WAVE_OPACITY_DECAY_VELOCITY = 1.6f / GLOBAL_SPEED;
45    private static final float WAVE_OUTER_OPACITY_VELOCITY = 1.2f * GLOBAL_SPEED;
46
47    // Hardware animators.
48    private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>();
49    private final ArrayList<RenderNodeAnimator> mPendingAnimations = new ArrayList<>();
50
51    private final RippleDrawable mOwner;
52
53    /** Bounds used for computing max radius. */
54    private final Rect mBounds;
55
56    /** Full-opacity color for drawing this ripple. */
57    private int mColor;
58
59    /** Maximum ripple radius. */
60    private float mOuterRadius;
61
62    /** Screen density used to adjust pixel-based velocities. */
63    private float mDensity;
64
65    private float mStartingX;
66    private float mStartingY;
67
68    // Hardware rendering properties.
69    private CanvasProperty<Paint> mPropPaint;
70    private CanvasProperty<Float> mPropRadius;
71    private CanvasProperty<Float> mPropX;
72    private CanvasProperty<Float> mPropY;
73    private CanvasProperty<Paint> mPropOuterPaint;
74    private CanvasProperty<Float> mPropOuterRadius;
75    private CanvasProperty<Float> mPropOuterX;
76    private CanvasProperty<Float> mPropOuterY;
77
78    // Software animators.
79    private ObjectAnimator mAnimRadius;
80    private ObjectAnimator mAnimOpacity;
81    private ObjectAnimator mAnimOuterOpacity;
82    private ObjectAnimator mAnimX;
83    private ObjectAnimator mAnimY;
84
85    // Software rendering properties.
86    private float mOuterOpacity = 0;
87    private float mOpacity = 1;
88    private float mOuterX;
89    private float mOuterY;
90
91    // Values used to tween between the start and end positions.
92    private float mTweenRadius = 0;
93    private float mTweenX = 0;
94    private float mTweenY = 0;
95
96    /** Whether we should be drawing hardware animations. */
97    private boolean mHardwareAnimating;
98
99    /** Whether we can use hardware acceleration for the exit animation. */
100    private boolean mCanUseHardware;
101
102    /** Whether we have an explicit maximum radius. */
103    private boolean mHasMaxRadius;
104
105    /**
106     * Creates a new ripple.
107     */
108    public Ripple(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
109        mOwner = owner;
110        mBounds = bounds;
111        mStartingX = startingX;
112        mStartingY = startingY;
113    }
114
115    public void setup(int maxRadius, int color, float density) {
116        mColor = color | 0xFF000000;
117
118        if (maxRadius != RippleDrawable.RADIUS_AUTO) {
119            mHasMaxRadius = true;
120            mOuterRadius = maxRadius;
121        } else {
122            final float halfWidth = mBounds.width() / 2.0f;
123            final float halfHeight = mBounds.height() / 2.0f;
124            mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
125        }
126
127        mOuterX = 0;
128        mOuterY = 0;
129        mDensity = density;
130    }
131
132    public void onHotspotBoundsChanged() {
133        if (!mHasMaxRadius) {
134            final float halfWidth = mBounds.width() / 2.0f;
135            final float halfHeight = mBounds.height() / 2.0f;
136            mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
137        }
138    }
139
140    public void setOpacity(float a) {
141        mOpacity = a;
142        invalidateSelf();
143    }
144
145    public float getOpacity() {
146        return mOpacity;
147    }
148
149    public void setOuterOpacity(float a) {
150        mOuterOpacity = a;
151        invalidateSelf();
152    }
153
154    public float getOuterOpacity() {
155        return mOuterOpacity;
156    }
157
158    public void setRadiusGravity(float r) {
159        mTweenRadius = r;
160        invalidateSelf();
161    }
162
163    public float getRadiusGravity() {
164        return mTweenRadius;
165    }
166
167    public void setXGravity(float x) {
168        mTweenX = x;
169        invalidateSelf();
170    }
171
172    public float getXGravity() {
173        return mTweenX;
174    }
175
176    public void setYGravity(float y) {
177        mTweenY = y;
178        invalidateSelf();
179    }
180
181    public float getYGravity() {
182        return mTweenY;
183    }
184
185    /**
186     * Draws the ripple centered at (0,0) using the specified paint.
187     */
188    public boolean draw(Canvas c, Paint p) {
189        final boolean canUseHardware = c.isHardwareAccelerated();
190        if (mCanUseHardware != canUseHardware && mCanUseHardware) {
191            // We've switched from hardware to non-hardware mode. Panic.
192            cancelHardwareAnimations();
193        }
194        mCanUseHardware = canUseHardware;
195
196        final boolean hasContent;
197        if (canUseHardware && mHardwareAnimating) {
198            hasContent = drawHardware((HardwareCanvas) c);
199        } else {
200            hasContent = drawSoftware(c, p);
201        }
202
203        return hasContent;
204    }
205
206    private boolean drawHardware(HardwareCanvas c) {
207        // If we have any pending hardware animations, cancel any running
208        // animations and start those now.
209        final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
210        final int N = pendingAnimations == null ? 0 : pendingAnimations.size();
211        if (N > 0) {
212            cancelHardwareAnimations();
213
214            for (int i = 0; i < N; i++) {
215                pendingAnimations.get(i).setTarget(c);
216                pendingAnimations.get(i).start();
217            }
218
219            mRunningAnimations.addAll(pendingAnimations);
220            pendingAnimations.clear();
221        }
222
223        c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
224        c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
225
226        return true;
227    }
228
229    private boolean drawSoftware(Canvas c, Paint p) {
230        boolean hasContent = false;
231
232        // Cache the paint alpha so we can restore it later.
233        final int paintAlpha = p.getAlpha();
234
235        final int outerAlpha = (int) (255 * mOuterOpacity + 0.5f);
236        if (outerAlpha > 0 && mOuterRadius > 0) {
237            p.setAlpha(outerAlpha);
238            p.setStyle(Style.FILL);
239            c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
240            hasContent = true;
241        }
242
243        final int alpha = (int) (255 * mOpacity + 0.5f);
244        final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
245        if (alpha > 0 && radius > 0) {
246            final float x = MathUtils.lerp(mStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
247            final float y = MathUtils.lerp(mStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
248            p.setAlpha(alpha);
249            p.setStyle(Style.FILL);
250            c.drawCircle(x, y, radius, p);
251            hasContent = true;
252        }
253
254        p.setAlpha(paintAlpha);
255
256        return hasContent;
257    }
258
259    /**
260     * Returns the maximum bounds of the ripple relative to the ripple center.
261     */
262    public void getBounds(Rect bounds) {
263        final int outerX = (int) mOuterX;
264        final int outerY = (int) mOuterY;
265        final int r = (int) mOuterRadius;
266        bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
267    }
268
269    /**
270     * Specifies the starting position relative to the drawable bounds. No-op if
271     * the ripple has already entered.
272     */
273    public void move(float x, float y) {
274        mStartingX = x;
275        mStartingY = y;
276    }
277
278    /**
279     * Starts the enter animation.
280     */
281    public void enter() {
282        final int radiusDuration = (int)
283                (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
284        final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY);
285
286        final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
287        radius.setAutoCancel(true);
288        radius.setDuration(radiusDuration);
289        radius.setInterpolator(LINEAR_INTERPOLATOR);
290
291        final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
292        cX.setAutoCancel(true);
293        cX.setDuration(radiusDuration);
294
295        final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
296        cY.setAutoCancel(true);
297        cY.setDuration(radiusDuration);
298
299        final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
300        outer.setAutoCancel(true);
301        outer.setDuration(outerDuration);
302        outer.setInterpolator(LINEAR_INTERPOLATOR);
303
304        mAnimRadius = radius;
305        mAnimOuterOpacity = outer;
306        mAnimX = cX;
307        mAnimY = cY;
308
309        // Enter animations always run on the UI thread, since it's unlikely
310        // that anything interesting is happening until the user lifts their
311        // finger.
312        radius.start();
313        outer.start();
314        cX.start();
315        cY.start();
316    }
317
318    /**
319     * Starts the exit animation.
320     */
321    public void exit() {
322        cancelSoftwareAnimations();
323
324        final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
325        final float remaining;
326        if (mAnimRadius != null && mAnimRadius.isRunning()) {
327            remaining = mOuterRadius - radius;
328        } else {
329            remaining = mOuterRadius;
330        }
331
332        final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
333                + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
334        final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
335
336        // Determine at what time the inner and outer opacity intersect.
337        // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
338        // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
339        final int outerInflection = Math.max(0, (int) (1000 * (mOpacity - mOuterOpacity)
340                / (WAVE_OPACITY_DECAY_VELOCITY + WAVE_OUTER_OPACITY_VELOCITY) + 0.5f));
341        final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection
342                * WAVE_OUTER_OPACITY_VELOCITY / 1000) + 0.5f);
343
344        if (mCanUseHardware) {
345            exitHardware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity);
346        } else {
347            exitSoftware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity);
348        }
349    }
350
351    private void exitHardware(int radiusDuration, int opacityDuration, int outerInflection,
352            int inflectionOpacity) {
353        mPendingAnimations.clear();
354
355        final float startX = MathUtils.lerp(mStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
356        final float startY = MathUtils.lerp(mStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
357        final Paint outerPaint = new Paint();
358        outerPaint.setAntiAlias(true);
359        outerPaint.setColor(mColor);
360        outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f));
361        outerPaint.setStyle(Style.FILL);
362        mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
363        mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
364        mPropOuterX = CanvasProperty.createFloat(mOuterX);
365        mPropOuterY = CanvasProperty.createFloat(mOuterY);
366
367        final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
368        final Paint paint = new Paint();
369        paint.setAntiAlias(true);
370        paint.setColor(mColor);
371        paint.setAlpha((int) (255 * mOpacity + 0.5f));
372        paint.setStyle(Style.FILL);
373        mPropPaint = CanvasProperty.createPaint(paint);
374        mPropRadius = CanvasProperty.createFloat(startRadius);
375        mPropX = CanvasProperty.createFloat(startX);
376        mPropY = CanvasProperty.createFloat(startY);
377
378        final RenderNodeAnimator radiusAnim = new RenderNodeAnimator(mPropRadius, mOuterRadius);
379        radiusAnim.setDuration(radiusDuration);
380        radiusAnim.setInterpolator(LINEAR_INTERPOLATOR);
381
382        final RenderNodeAnimator xAnim = new RenderNodeAnimator(mPropX, mOuterX);
383        xAnim.setDuration(radiusDuration);
384        xAnim.setInterpolator(LINEAR_INTERPOLATOR);
385
386        final RenderNodeAnimator yAnim = new RenderNodeAnimator(mPropY, mOuterY);
387        yAnim.setDuration(radiusDuration);
388        yAnim.setInterpolator(LINEAR_INTERPOLATOR);
389
390        final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPropPaint,
391                RenderNodeAnimator.PAINT_ALPHA, 0);
392        opacityAnim.setDuration(opacityDuration);
393        opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
394
395        final RenderNodeAnimator outerOpacityAnim;
396        if (outerInflection > 0) {
397            // Outer opacity continues to increase for a bit.
398            outerOpacityAnim = new RenderNodeAnimator(
399                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
400            outerOpacityAnim.setDuration(outerInflection);
401            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
402
403            // Chain the outer opacity exit animation.
404            final int outerDuration = opacityDuration - outerInflection;
405            if (outerDuration > 0) {
406                final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
407                        mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
408                outerFadeOutAnim.setDuration(outerDuration);
409                outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
410                outerFadeOutAnim.setStartDelay(outerInflection);
411                outerFadeOutAnim.addListener(mAnimationListener);
412
413                mPendingAnimations.add(outerFadeOutAnim);
414            } else {
415                outerOpacityAnim.addListener(mAnimationListener);
416            }
417        } else {
418            outerOpacityAnim = new RenderNodeAnimator(
419                    mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
420            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
421            outerOpacityAnim.setDuration(opacityDuration);
422            outerOpacityAnim.addListener(mAnimationListener);
423        }
424
425        mPendingAnimations.add(radiusAnim);
426        mPendingAnimations.add(opacityAnim);
427        mPendingAnimations.add(outerOpacityAnim);
428        mPendingAnimations.add(xAnim);
429        mPendingAnimations.add(yAnim);
430
431        mHardwareAnimating = true;
432
433        invalidateSelf();
434    }
435
436    private void exitSoftware(int radiusDuration, int opacityDuration, int outerInflection,
437            int inflectionOpacity) {
438        final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
439        radiusAnim.setAutoCancel(true);
440        radiusAnim.setDuration(radiusDuration);
441        radiusAnim.setInterpolator(LINEAR_INTERPOLATOR);
442
443        final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
444        xAnim.setAutoCancel(true);
445        xAnim.setDuration(radiusDuration);
446        xAnim.setInterpolator(LINEAR_INTERPOLATOR);
447
448        final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
449        yAnim.setAutoCancel(true);
450        yAnim.setDuration(radiusDuration);
451        yAnim.setInterpolator(LINEAR_INTERPOLATOR);
452
453        final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0);
454        opacityAnim.setAutoCancel(true);
455        opacityAnim.setDuration(opacityDuration);
456        opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
457
458        final ObjectAnimator outerOpacityAnim;
459        if (outerInflection > 0) {
460            // Outer opacity continues to increase for a bit.
461            outerOpacityAnim = ObjectAnimator.ofFloat(this,
462                    "outerOpacity", inflectionOpacity / 255.0f);
463            outerOpacityAnim.setAutoCancel(true);
464            outerOpacityAnim.setDuration(outerInflection);
465            outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
466
467            // Chain the outer opacity exit animation.
468            final int outerDuration = opacityDuration - outerInflection;
469            if (outerDuration > 0) {
470                outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
471                    @Override
472                    public void onAnimationEnd(Animator animation) {
473                        final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(Ripple.this,
474                                "outerOpacity", 0);
475                        outerFadeOutAnim.setAutoCancel(true);
476                        outerFadeOutAnim.setDuration(outerDuration);
477                        outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
478                        outerFadeOutAnim.addListener(mAnimationListener);
479
480                        mAnimOuterOpacity = outerFadeOutAnim;
481
482                        outerFadeOutAnim.start();
483                    }
484
485                    @Override
486                    public void onAnimationCancel(Animator animation) {
487                        animation.removeListener(this);
488                    }
489                });
490            } else {
491                outerOpacityAnim.addListener(mAnimationListener);
492            }
493        } else {
494            outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
495            outerOpacityAnim.setAutoCancel(true);
496            outerOpacityAnim.setDuration(opacityDuration);
497            outerOpacityAnim.addListener(mAnimationListener);
498        }
499
500        mAnimRadius = radiusAnim;
501        mAnimOpacity = opacityAnim;
502        mAnimOuterOpacity = outerOpacityAnim;
503        mAnimX = opacityAnim;
504        mAnimY = opacityAnim;
505
506        radiusAnim.start();
507        opacityAnim.start();
508        outerOpacityAnim.start();
509        xAnim.start();
510        yAnim.start();
511    }
512
513    /**
514     * Cancel all animations.
515     */
516    public void cancel() {
517        cancelSoftwareAnimations();
518        cancelHardwareAnimations();
519    }
520
521    private void cancelSoftwareAnimations() {
522        if (mAnimRadius != null) {
523            mAnimRadius.cancel();
524        }
525
526        if (mAnimOpacity != null) {
527            mAnimOpacity.cancel();
528        }
529
530        if (mAnimOuterOpacity != null) {
531            mAnimOuterOpacity.cancel();
532        }
533
534        if (mAnimX != null) {
535            mAnimX.cancel();
536        }
537
538        if (mAnimY != null) {
539            mAnimY.cancel();
540        }
541    }
542
543    /**
544     * Cancels any running hardware animations.
545     */
546    private void cancelHardwareAnimations() {
547        final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
548        final int N = runningAnimations == null ? 0 : runningAnimations.size();
549        for (int i = 0; i < N; i++) {
550            runningAnimations.get(i).cancel();
551        }
552
553        runningAnimations.clear();
554    }
555
556    private void removeSelf() {
557        // The owner will invalidate itself.
558        mOwner.removeRipple(this);
559    }
560
561    private void invalidateSelf() {
562        mOwner.invalidateSelf();
563    }
564
565    private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
566        @Override
567        public void onAnimationEnd(Animator animation) {
568            removeSelf();
569        }
570    };
571}
572