RippleForeground.java revision 6a67db41388165aca63d0d5de2830cc096ed930b
1/*
2 * Copyright (C) 2015 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.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.animation.TimeInterpolator;
24import android.graphics.Canvas;
25import android.graphics.CanvasProperty;
26import android.graphics.Paint;
27import android.graphics.Rect;
28import android.util.FloatProperty;
29import android.util.MathUtils;
30import android.view.HardwareCanvas;
31import android.view.RenderNodeAnimator;
32import android.view.animation.LinearInterpolator;
33
34/**
35 * Draws a ripple foreground.
36 */
37class RippleForeground extends RippleComponent {
38    private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
39    private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator(
40            400f, 1.4f, 0);
41
42    // Pixel-based accelerations and velocities.
43    private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024;
44    private static final float WAVE_TOUCH_UP_ACCELERATION = 3400;
45    private static final float WAVE_OPACITY_DECAY_VELOCITY = 3;
46
47    // Bounded ripple animation properties.
48    private static final int BOUNDED_ORIGIN_EXIT_DURATION = 300;
49    private static final int BOUNDED_RADIUS_EXIT_DURATION = 800;
50    private static final int BOUNDED_OPACITY_EXIT_DURATION = 400;
51    private static final float MAX_BOUNDED_RADIUS = 350;
52
53    private static final int RIPPLE_ENTER_DELAY = 80;
54    private static final int OPACITY_ENTER_DURATION_FAST = 120;
55
56    // Parent-relative values for starting position.
57    private float mStartingX;
58    private float mStartingY;
59    private float mClampedStartingX;
60    private float mClampedStartingY;
61
62    // Hardware rendering properties.
63    private CanvasProperty<Paint> mPropPaint;
64    private CanvasProperty<Float> mPropRadius;
65    private CanvasProperty<Float> mPropX;
66    private CanvasProperty<Float> mPropY;
67
68    // Target values for tween animations.
69    private float mTargetX = 0;
70    private float mTargetY = 0;
71
72    /** Ripple target radius used when bounded. Not used for clamping. */
73    private float mBoundedRadius = 0;
74
75    // Software rendering properties.
76    private float mOpacity = 1;
77
78    // Values used to tween between the start and end positions.
79    private float mTweenRadius = 0;
80    private float mTweenX = 0;
81    private float mTweenY = 0;
82
83    /** Whether this ripple is bounded. */
84    private boolean mIsBounded;
85
86    /** Whether this ripple has finished its exit animation. */
87    private boolean mHasFinishedExit;
88
89    public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY,
90            boolean isBounded) {
91        super(owner, bounds);
92
93        mIsBounded = isBounded;
94        mStartingX = startingX;
95        mStartingY = startingY;
96
97        if (isBounded) {
98            mBoundedRadius = MAX_BOUNDED_RADIUS * 0.9f
99                    + (float) (MAX_BOUNDED_RADIUS * Math.random() * 0.1);
100        } else {
101            mBoundedRadius = 0;
102        }
103    }
104
105    @Override
106    protected void onTargetRadiusChanged(float targetRadius) {
107        clampStartingPosition();
108    }
109
110    @Override
111    protected boolean drawSoftware(Canvas c, Paint p) {
112        boolean hasContent = false;
113
114        final int origAlpha = p.getAlpha();
115        final int alpha = (int) (origAlpha * mOpacity + 0.5f);
116        final float radius = getCurrentRadius();
117        if (alpha > 0 && radius > 0) {
118            final float x = getCurrentX();
119            final float y = getCurrentY();
120            p.setAlpha(alpha);
121            c.drawCircle(x, y, radius, p);
122            p.setAlpha(origAlpha);
123            hasContent = true;
124        }
125
126        return hasContent;
127    }
128
129    @Override
130    protected boolean drawHardware(HardwareCanvas c) {
131        c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
132        return true;
133    }
134
135    /**
136     * Returns the maximum bounds of the ripple relative to the ripple center.
137     */
138    public void getBounds(Rect bounds) {
139        final int outerX = (int) mTargetX;
140        final int outerY = (int) mTargetY;
141        final int r = (int) mTargetRadius + 1;
142        bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
143    }
144
145    /**
146     * Specifies the starting position relative to the drawable bounds. No-op if
147     * the ripple has already entered.
148     */
149    public void move(float x, float y) {
150        mStartingX = x;
151        mStartingY = y;
152
153        clampStartingPosition();
154    }
155
156    /**
157     * @return {@code true} if this ripple has finished its exit animation
158     */
159    public boolean hasFinishedExit() {
160        return mHasFinishedExit;
161    }
162
163    @Override
164    protected Animator createSoftwareEnter(boolean fast) {
165        // Bounded ripples don't have enter animations.
166        if (mIsBounded) {
167            return null;
168        }
169
170        final int duration = (int)
171                (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
172
173        final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
174        tweenRadius.setAutoCancel(true);
175        tweenRadius.setDuration(duration);
176        tweenRadius.setInterpolator(LINEAR_INTERPOLATOR);
177        tweenRadius.setStartDelay(RIPPLE_ENTER_DELAY);
178
179        final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
180        tweenOrigin.setAutoCancel(true);
181        tweenOrigin.setDuration(duration);
182        tweenOrigin.setInterpolator(LINEAR_INTERPOLATOR);
183        tweenOrigin.setStartDelay(RIPPLE_ENTER_DELAY);
184
185        final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
186        opacity.setAutoCancel(true);
187        opacity.setDuration(OPACITY_ENTER_DURATION_FAST);
188        opacity.setInterpolator(LINEAR_INTERPOLATOR);
189
190        final AnimatorSet set = new AnimatorSet();
191        set.play(tweenOrigin).with(tweenRadius).with(opacity);
192
193        return set;
194    }
195
196    private float getCurrentX() {
197        return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX);
198    }
199
200    private float getCurrentY() {
201        return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY);
202    }
203
204    private int getRadiusExitDuration() {
205        final float remainingRadius = mTargetRadius - getCurrentRadius();
206        return (int) (1000 * Math.sqrt(remainingRadius / (WAVE_TOUCH_UP_ACCELERATION
207                + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
208    }
209
210    private float getCurrentRadius() {
211        return MathUtils.lerp(0, mTargetRadius, mTweenRadius);
212    }
213
214    private int getOpacityExitDuration() {
215        return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
216    }
217
218    /**
219     * Compute target values that are dependent on bounding.
220     */
221    private void computeBoundedTargetValues() {
222        mTargetX = (mClampedStartingX - mBounds.exactCenterX()) * .7f;
223        mTargetY = (mClampedStartingY - mBounds.exactCenterY()) * .7f;
224        mTargetRadius = mBoundedRadius;
225    }
226
227    @Override
228    protected Animator createSoftwareExit() {
229        final int radiusDuration;
230        final int originDuration;
231        final int opacityDuration;
232        if (mIsBounded) {
233            computeBoundedTargetValues();
234
235            radiusDuration = BOUNDED_RADIUS_EXIT_DURATION;
236            originDuration = BOUNDED_ORIGIN_EXIT_DURATION;
237            opacityDuration = BOUNDED_OPACITY_EXIT_DURATION;
238        } else {
239            radiusDuration = getRadiusExitDuration();
240            originDuration = radiusDuration;
241            opacityDuration = getOpacityExitDuration();
242        }
243
244        final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
245        tweenRadius.setAutoCancel(true);
246        tweenRadius.setDuration(radiusDuration);
247        tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
248
249        final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
250        tweenOrigin.setAutoCancel(true);
251        tweenOrigin.setDuration(originDuration);
252        tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR);
253
254        final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0);
255        opacity.setAutoCancel(true);
256        opacity.setDuration(opacityDuration);
257        opacity.setInterpolator(LINEAR_INTERPOLATOR);
258
259        final AnimatorSet set = new AnimatorSet();
260        set.play(tweenOrigin).with(tweenRadius).with(opacity);
261        set.addListener(mAnimationListener);
262
263        return set;
264    }
265
266    @Override
267    protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
268        final int radiusDuration;
269        final int originDuration;
270        final int opacityDuration;
271        if (mIsBounded) {
272            computeBoundedTargetValues();
273
274            radiusDuration = BOUNDED_RADIUS_EXIT_DURATION;
275            originDuration = BOUNDED_ORIGIN_EXIT_DURATION;
276            opacityDuration = BOUNDED_OPACITY_EXIT_DURATION;
277        } else {
278            radiusDuration = getRadiusExitDuration();
279            originDuration = radiusDuration;
280            opacityDuration = getOpacityExitDuration();
281        }
282
283        final float startX = getCurrentX();
284        final float startY = getCurrentY();
285        final float startRadius = getCurrentRadius();
286
287        p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f));
288
289        mPropPaint = CanvasProperty.createPaint(p);
290        mPropRadius = CanvasProperty.createFloat(startRadius);
291        mPropX = CanvasProperty.createFloat(startX);
292        mPropY = CanvasProperty.createFloat(startY);
293
294        final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
295        radius.setDuration(radiusDuration);
296        radius.setInterpolator(DECELERATE_INTERPOLATOR);
297
298        final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX);
299        x.setDuration(originDuration);
300        x.setInterpolator(DECELERATE_INTERPOLATOR);
301
302        final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
303        y.setDuration(originDuration);
304        y.setInterpolator(DECELERATE_INTERPOLATOR);
305
306        final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
307                RenderNodeAnimator.PAINT_ALPHA, 0);
308        opacity.setDuration(opacityDuration);
309        opacity.setInterpolator(LINEAR_INTERPOLATOR);
310        opacity.addListener(mAnimationListener);
311
312        final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
313        set.add(radius);
314        set.add(opacity);
315        set.add(x);
316        set.add(y);
317
318        return set;
319    }
320
321    @Override
322    protected void jumpValuesToExit() {
323        mOpacity = 0;
324        mTweenX = 1;
325        mTweenY = 1;
326        mTweenRadius = 1;
327    }
328
329    /**
330     * Clamps the starting position to fit within the ripple bounds.
331     */
332    private void clampStartingPosition() {
333        final float cX = mBounds.exactCenterX();
334        final float cY = mBounds.exactCenterY();
335        final float dX = mStartingX - cX;
336        final float dY = mStartingY - cY;
337        final float r = mTargetRadius;
338        if (dX * dX + dY * dY > r * r) {
339            // Point is outside the circle, clamp to the perimeter.
340            final double angle = Math.atan2(dY, dX);
341            mClampedStartingX = cX + (float) (Math.cos(angle) * r);
342            mClampedStartingY = cY + (float) (Math.sin(angle) * r);
343        } else {
344            mClampedStartingX = mStartingX;
345            mClampedStartingY = mStartingY;
346        }
347    }
348
349    private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
350        @Override
351        public void onAnimationEnd(Animator animator) {
352            mHasFinishedExit = true;
353        }
354    };
355
356    /**
357    * Interpolator with a smooth log deceleration.
358    */
359    private static final class LogDecelerateInterpolator implements TimeInterpolator {
360        private final float mBase;
361        private final float mDrift;
362        private final float mTimeScale;
363        private final float mOutputScale;
364
365        public LogDecelerateInterpolator(float base, float timeScale, float drift) {
366            mBase = base;
367            mDrift = drift;
368            mTimeScale = 1f / timeScale;
369
370            mOutputScale = 1f / computeLog(1f);
371        }
372
373        private float computeLog(float t) {
374            return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t);
375        }
376
377        @Override
378        public float getInterpolation(float t) {
379            return computeLog(t) * mOutputScale;
380        }
381    }
382
383    /**
384     * Property for animating radius between its initial and target values.
385     */
386    private static final FloatProperty<RippleForeground> TWEEN_RADIUS =
387            new FloatProperty<RippleForeground>("tweenRadius") {
388        @Override
389        public void setValue(RippleForeground object, float value) {
390            object.mTweenRadius = value;
391            object.invalidateSelf();
392        }
393
394        @Override
395        public Float get(RippleForeground object) {
396            return object.mTweenRadius;
397        }
398    };
399
400    /**
401     * Property for animating origin between its initial and target values.
402     */
403    private static final FloatProperty<RippleForeground> TWEEN_ORIGIN =
404            new FloatProperty<RippleForeground>("tweenOrigin") {
405                @Override
406                public void setValue(RippleForeground object, float value) {
407                    object.mTweenX = value;
408                    object.mTweenY = value;
409                    object.invalidateSelf();
410                }
411
412                @Override
413                public Float get(RippleForeground object) {
414                    return object.mTweenX;
415                }
416            };
417
418    /**
419     * Property for animating opacity between 0 and its target value.
420     */
421    private static final FloatProperty<RippleForeground> OPACITY =
422            new FloatProperty<RippleForeground>("opacity") {
423        @Override
424        public void setValue(RippleForeground object, float value) {
425            object.mOpacity = value;
426            object.invalidateSelf();
427        }
428
429        @Override
430        public Float get(RippleForeground object) {
431            return object.mOpacity;
432        }
433    };
434}
435