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