RippleForeground.java revision f6829a0a618b4523619ec53c996b04d67e3186b9
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.DisplayListCanvas;
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    private static final int RIPPLE_ENTER_DELAY = 80;
48    private static final int OPACITY_ENTER_DURATION_FAST = 120;
49
50    private float mStartingX;
51    private float mStartingY;
52    private float mClampedStartingX;
53    private float mClampedStartingY;
54
55    // Hardware rendering properties.
56    private CanvasProperty<Paint> mPropPaint;
57    private CanvasProperty<Float> mPropRadius;
58    private CanvasProperty<Float> mPropX;
59    private CanvasProperty<Float> mPropY;
60
61    // Software rendering properties.
62    private float mOpacity = 1;
63    private float mOuterX;
64    private float mOuterY;
65
66    // Values used to tween between the start and end positions.
67    private float mTweenRadius = 0;
68    private float mTweenX = 0;
69    private float mTweenY = 0;
70
71    /** Whether this ripple has finished its exit animation. */
72    private boolean mHasFinishedExit;
73
74    public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
75        super(owner, bounds);
76
77        mStartingX = startingX;
78        mStartingY = startingY;
79    }
80
81    @Override
82    public void onSetup() {
83        mOuterX = 0;
84        mOuterY = 0;
85    }
86
87    @Override
88    protected void onTargetRadiusChanged(float targetRadius) {
89        clampStartingPosition();
90    }
91
92    @Override
93    protected boolean drawSoftware(Canvas c, Paint p) {
94        boolean hasContent = false;
95
96        final int origAlpha = p.getAlpha();
97        final int alpha = (int) (origAlpha * mOpacity + 0.5f);
98        final float radius = MathUtils.lerp(0, mTargetRadius, mTweenRadius);
99        if (alpha > 0 && radius > 0) {
100            final float x = MathUtils.lerp(
101                    mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
102            final float y = MathUtils.lerp(
103                    mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
104            p.setAlpha(alpha);
105            c.drawCircle(x, y, radius, p);
106            p.setAlpha(origAlpha);
107            hasContent = true;
108        }
109
110        return hasContent;
111    }
112
113    @Override
114    protected boolean drawHardware(DisplayListCanvas c) {
115        c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
116        return true;
117    }
118
119    /**
120     * Returns the maximum bounds of the ripple relative to the ripple center.
121     */
122    public void getBounds(Rect bounds) {
123        final int outerX = (int) mOuterX;
124        final int outerY = (int) mOuterY;
125        final int r = (int) mTargetRadius + 1;
126        bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
127    }
128
129    /**
130     * Specifies the starting position relative to the drawable bounds. No-op if
131     * the ripple has already entered.
132     */
133    public void move(float x, float y) {
134        mStartingX = x;
135        mStartingY = y;
136
137        clampStartingPosition();
138    }
139
140    /**
141     * @return {@code true} if this ripple has finished its exit animation
142     */
143    public boolean hasFinishedExit() {
144        return mHasFinishedExit;
145    }
146
147    @Override
148    protected Animator createSoftwareEnter(boolean fast) {
149        final int duration = (int)
150                (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
151
152        final ObjectAnimator tweenAll = ObjectAnimator.ofFloat(this, TWEEN_ALL, 1);
153        tweenAll.setAutoCancel(true);
154        tweenAll.setDuration(duration);
155        tweenAll.setInterpolator(LINEAR_INTERPOLATOR);
156        tweenAll.setStartDelay(RIPPLE_ENTER_DELAY);
157
158        final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
159        opacity.setAutoCancel(true);
160        opacity.setDuration(OPACITY_ENTER_DURATION_FAST);
161        opacity.setInterpolator(LINEAR_INTERPOLATOR);
162
163        final AnimatorSet set = new AnimatorSet();
164        set.play(tweenAll).with(opacity);
165
166        return set;
167    }
168
169    private int getRadiusExitDuration() {
170        final float radius = MathUtils.lerp(0, mTargetRadius, mTweenRadius);
171        final float remaining = mTargetRadius - radius;
172        return (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
173                + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
174    }
175
176    private int getOpacityExitDuration() {
177        return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
178    }
179
180    @Override
181    protected Animator createSoftwareExit() {
182        final int radiusDuration = getRadiusExitDuration();
183        final int opacityDuration = getOpacityExitDuration();
184
185        final ObjectAnimator tweenAll = ObjectAnimator.ofFloat(this, TWEEN_ALL, 1);
186        tweenAll.setAutoCancel(true);
187        tweenAll.setDuration(radiusDuration);
188        tweenAll.setInterpolator(DECELERATE_INTERPOLATOR);
189
190        final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0);
191        opacity.setAutoCancel(true);
192        opacity.setDuration(opacityDuration);
193        opacity.setInterpolator(LINEAR_INTERPOLATOR);
194
195        final AnimatorSet set = new AnimatorSet();
196        set.play(tweenAll).with(opacity);
197        set.addListener(mAnimationListener);
198
199        return set;
200    }
201
202    @Override
203    protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
204        final int radiusDuration = getRadiusExitDuration();
205        final int opacityDuration = getOpacityExitDuration();
206
207        final float startX = MathUtils.lerp(
208                mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
209        final float startY = MathUtils.lerp(
210                mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
211
212        final float startRadius = MathUtils.lerp(0, mTargetRadius, mTweenRadius);
213        p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f));
214
215        mPropPaint = CanvasProperty.createPaint(p);
216        mPropRadius = CanvasProperty.createFloat(startRadius);
217        mPropX = CanvasProperty.createFloat(startX);
218        mPropY = CanvasProperty.createFloat(startY);
219
220        final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
221        radius.setDuration(radiusDuration);
222        radius.setInterpolator(DECELERATE_INTERPOLATOR);
223
224        final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mOuterX);
225        x.setDuration(radiusDuration);
226        x.setInterpolator(DECELERATE_INTERPOLATOR);
227
228        final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mOuterY);
229        y.setDuration(radiusDuration);
230        y.setInterpolator(DECELERATE_INTERPOLATOR);
231
232        final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
233                RenderNodeAnimator.PAINT_ALPHA, 0);
234        opacity.setDuration(opacityDuration);
235        opacity.setInterpolator(LINEAR_INTERPOLATOR);
236        opacity.addListener(mAnimationListener);
237
238        final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
239        set.add(radius);
240        set.add(opacity);
241        set.add(x);
242        set.add(y);
243
244        return set;
245    }
246
247    @Override
248    protected void jumpValuesToExit() {
249        mOpacity = 0;
250        mTweenX = 1;
251        mTweenY = 1;
252        mTweenRadius = 1;
253    }
254
255    /**
256     * Clamps the starting position to fit within the ripple bounds.
257     */
258    private void clampStartingPosition() {
259        final float cX = mBounds.exactCenterX();
260        final float cY = mBounds.exactCenterY();
261        final float dX = mStartingX - cX;
262        final float dY = mStartingY - cY;
263        final float r = mTargetRadius;
264        if (dX * dX + dY * dY > r * r) {
265            // Point is outside the circle, clamp to the perimeter.
266            final double angle = Math.atan2(dY, dX);
267            mClampedStartingX = cX + (float) (Math.cos(angle) * r);
268            mClampedStartingY = cY + (float) (Math.sin(angle) * r);
269        } else {
270            mClampedStartingX = mStartingX;
271            mClampedStartingY = mStartingY;
272        }
273    }
274
275    private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
276        @Override
277        public void onAnimationEnd(Animator animator) {
278            mHasFinishedExit = true;
279        }
280    };
281
282    /**
283    * Interpolator with a smooth log deceleration.
284    */
285    private static final class LogDecelerateInterpolator implements TimeInterpolator {
286        private final float mBase;
287        private final float mDrift;
288        private final float mTimeScale;
289        private final float mOutputScale;
290
291        public LogDecelerateInterpolator(float base, float timeScale, float drift) {
292            mBase = base;
293            mDrift = drift;
294            mTimeScale = 1f / timeScale;
295
296            mOutputScale = 1f / computeLog(1f);
297        }
298
299        private float computeLog(float t) {
300            return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t);
301        }
302
303        @Override
304        public float getInterpolation(float t) {
305            return computeLog(t) * mOutputScale;
306        }
307    }
308
309    /**
310     * Property for animating radius, center X, and center Y between their
311     * initial and target values.
312     */
313    private static final FloatProperty<RippleForeground> TWEEN_ALL =
314            new FloatProperty<RippleForeground>("tweenAll") {
315        @Override
316        public void setValue(RippleForeground object, float value) {
317            object.mTweenRadius = value;
318            object.mTweenX = value;
319            object.mTweenY = value;
320            object.invalidateSelf();
321        }
322
323        @Override
324        public Float get(RippleForeground object) {
325            return object.mTweenRadius;
326        }
327    };
328
329    /**
330     * Property for animating opacity between 0 and its target value.
331     */
332    private static final FloatProperty<RippleForeground> OPACITY =
333            new FloatProperty<RippleForeground>("opacity") {
334        @Override
335        public void setValue(RippleForeground object, float value) {
336            object.mOpacity = value;
337            object.invalidateSelf();
338        }
339
340        @Override
341        public Float get(RippleForeground object) {
342            return object.mOpacity;
343        }
344    };
345}
346