Ripple.java revision c3f35b01b5a21e110ca4eedf09c8c6164ab85dfb
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.TimeInterpolator;
20import android.graphics.Canvas;
21import android.graphics.Paint;
22import android.graphics.Paint.Style;
23import android.graphics.Rect;
24import android.util.MathUtils;
25import android.view.animation.AnimationUtils;
26import android.view.animation.DecelerateInterpolator;
27
28/**
29 * Draws a Quantum Paper ripple.
30 */
31class Ripple {
32    private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(2.0f);
33
34    /** Starting radius for a ripple. */
35    private static final int STARTING_RADIUS_DP = 16;
36
37    /** Radius when finger is outside view bounds. */
38    private static final int OUTSIDE_RADIUS_DP = 16;
39
40    /** Margin when constraining outside touches (fraction of outer radius). */
41    private static final float OUTSIDE_MARGIN = 0.8f;
42
43    /** Resistance factor when constraining outside touches. */
44    private static final float OUTSIDE_RESISTANCE = 0.7f;
45
46    /** Minimum alpha value during a pulse animation. */
47    private static final int PULSE_MIN_ALPHA = 128;
48
49    private final Rect mBounds;
50    private final Rect mPadding;
51
52    private RippleAnimator mAnimator;
53
54    private int mMinRadius;
55    private int mOutsideRadius;
56
57    /** Center x-coordinate. */
58    private float mX;
59
60    /** Center y-coordinate. */
61    private float mY;
62
63    /** Whether the center is within the parent bounds. */
64    private boolean mInside;
65
66    /** Whether to pulse this ripple. */
67    boolean mPulse;
68
69    /** Enter state. A value in [0...1] or -1 if not set. */
70    float mEnterState = -1;
71
72    /** Exit state. A value in [0...1] or -1 if not set. */
73    float mExitState = -1;
74
75    /** Outside state. A value in [0...1] or -1 if not set. */
76    float mOutsideState = -1;
77
78    /** Pulse state. A value in [0...1] or -1 if not set. */
79    float mPulseState = -1;
80
81    /**
82     * Creates a new ripple with the specified parent bounds, padding, initial
83     * position, and screen density.
84     */
85    public Ripple(Rect bounds, Rect padding, float x, float y, float density, boolean pulse) {
86        mBounds = bounds;
87        mPadding = padding;
88        mInside = mBounds.contains((int) x, (int) y);
89        mPulse = pulse;
90
91        mX = x;
92        mY = y;
93
94        mMinRadius = (int) (density * STARTING_RADIUS_DP + 0.5f);
95        mOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f);
96    }
97
98    public void setMinRadius(int minRadius) {
99        mMinRadius = minRadius;
100    }
101
102    public void setOutsideRadius(int outsideRadius) {
103        mOutsideRadius = outsideRadius;
104    }
105
106    /**
107     * Updates the center coordinates.
108     */
109    public void move(float x, float y) {
110        mX = x;
111        mY = y;
112
113        final boolean inside = mBounds.contains((int) x, (int) y);
114        if (mInside != inside) {
115            if (mAnimator != null) {
116                mAnimator.outside();
117            }
118            mInside = inside;
119        }
120    }
121
122    public void onBoundsChanged() {
123        final boolean inside = mBounds.contains((int) mX, (int) mY);
124        if (mInside != inside) {
125            if (mAnimator != null) {
126                mAnimator.outside();
127            }
128            mInside = inside;
129        }
130    }
131
132    public RippleAnimator animate() {
133        if (mAnimator == null) {
134            mAnimator = new RippleAnimator(this);
135        }
136        return mAnimator;
137    }
138
139    public boolean draw(Canvas c, Paint p) {
140        final Rect bounds = mBounds;
141        final Rect padding = mPadding;
142        final float dX = Math.max(mX, bounds.right - mX);
143        final float dY = Math.max(mY, bounds.bottom - mY);
144        final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY));
145
146        final float enterState = mEnterState;
147        final float exitState = mExitState;
148        final float outsideState = mOutsideState;
149        final float pulseState = mPulseState;
150        final float insideRadius = MathUtils.lerp(mMinRadius, maxRadius, enterState);
151        final float outerRadius = MathUtils.lerp(mOutsideRadius, insideRadius,
152                mInside ? outsideState : 1 - outsideState);
153
154        // Apply resistance effect when outside bounds.
155        final float x = looseConstrain(mX, bounds.left + padding.left, bounds.right - padding.right,
156                outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);
157        final float y = looseConstrain(mY, bounds.top + padding.top, bounds.bottom - padding.bottom,
158                outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE);
159
160        // Compute maximum alpha, taking pulse into account when active.
161        final int maxAlpha;
162        if (pulseState < 0 || pulseState >= 1) {
163            maxAlpha = 255;
164        } else {
165            final float pulseAlpha;
166            if (pulseState > 0.5) {
167                // Pulsing in to max alpha.
168                pulseAlpha = MathUtils.lerp(PULSE_MIN_ALPHA, 255, (pulseState - .5f) * 2);
169            } else {
170                // Pulsing out to min alpha.
171                pulseAlpha = MathUtils.lerp(255, PULSE_MIN_ALPHA, pulseState * 2f);
172            }
173
174            if (exitState > 0) {
175                // Animating exit, interpolate pulse with exit state.
176                maxAlpha = (int) (MathUtils.lerp(255, pulseAlpha, exitState) + 0.5f);
177            } else if (mInside) {
178                // No animation, no need to interpolate.
179                maxAlpha = (int) (pulseAlpha + 0.5f);
180            } else {
181                // Animating inside, interpolate pulse with inside state.
182                maxAlpha = (int) (MathUtils.lerp(pulseAlpha, 255, outsideState) + 0.5f);
183            }
184        }
185
186        if (maxAlpha > 0) {
187            if (exitState <= 0) {
188                // Exit state isn't showing, so we can simplify to a solid
189                // circle.
190                if (outerRadius > 0) {
191                    p.setAlpha(maxAlpha);
192                    p.setStyle(Style.FILL);
193                    c.drawCircle(x, y, outerRadius, p);
194                    return true;
195                }
196            } else {
197                // Both states are showing, so we need a circular stroke.
198                final float innerRadius = MathUtils.lerp(0, outerRadius, exitState);
199                final float strokeWidth = outerRadius - innerRadius;
200                if (strokeWidth > 0) {
201                    final float strokeRadius = innerRadius + strokeWidth / 2f;
202                    final int alpha = (int) (MathUtils.lerp(maxAlpha, 0, exitState) + 0.5f);
203                    if (alpha > 0) {
204                        p.setAlpha(alpha);
205                        p.setStyle(Style.STROKE);
206                        p.setStrokeWidth(strokeWidth);
207                        c.drawCircle(x, y, strokeRadius, p);
208                        return true;
209                    }
210                }
211            }
212        }
213
214        return false;
215    }
216
217    public void getBounds(Rect bounds) {
218        final int x = (int) mX;
219        final int y = (int) mY;
220        final int dX = Math.max(x, mBounds.right - x);
221        final int dY = Math.max(x, mBounds.bottom - y);
222        final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY));
223        bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius);
224    }
225
226    /**
227     * Constrains a value within a specified asymptotic margin outside a minimum
228     * and maximum.
229     */
230    private static float looseConstrain(float value, float min, float max, float margin,
231            float factor) {
232        if (value < min) {
233            return min - Math.min(margin, (float) Math.pow(min - value, factor));
234        } else if (value > max) {
235            return max + Math.min(margin, (float) Math.pow(value - max, factor));
236        } else {
237            return value;
238        }
239    }
240
241    public static class RippleAnimator {
242        /** Duration for animating the trailing edge of the ripple. */
243        private static final int EXIT_DURATION = 600;
244
245        /** Duration for animating the leading edge of the ripple. */
246        private static final int ENTER_DURATION = 400;
247
248        /** Minimum elapsed time between start of enter and exit animations. */
249        private static final int EXIT_MIN_DELAY = 200;
250
251        /** Duration for animating between inside and outside touch. */
252        private static final int OUTSIDE_DURATION = 300;
253
254        /** Duration for animating pulses. */
255        private static final int PULSE_DURATION = 400;
256
257        /** Interval between pulses while inside and fully entered. */
258        private static final int PULSE_INTERVAL = 400;
259
260        /** Delay before pulses start. */
261        private static final int PULSE_DELAY = 500;
262
263        /** The target ripple being animated. */
264        private final Ripple mTarget;
265
266        /** When the ripple started appearing. */
267        private long mEnterTime = -1;
268
269        /** When the ripple started vanishing. */
270        private long mExitTime = -1;
271
272        /** When the ripple last transitioned between inside and outside touch. */
273        private long mOutsideTime = -1;
274
275        public RippleAnimator(Ripple target) {
276            mTarget = target;
277        }
278
279        /**
280         * Starts the enter animation.
281         */
282        public void enter() {
283            mEnterTime = AnimationUtils.currentAnimationTimeMillis();
284        }
285
286        /**
287         * Starts the exit animation. If {@link #enter()} was called recently, the
288         * animation may be postponed.
289         */
290        public void exit() {
291            final long minTime = mEnterTime + EXIT_MIN_DELAY;
292            mExitTime = Math.max(minTime, AnimationUtils.currentAnimationTimeMillis());
293        }
294
295        /**
296         * Starts the outside transition animation.
297         */
298        public void outside() {
299            mOutsideTime = AnimationUtils.currentAnimationTimeMillis();
300        }
301
302        /**
303         * Returns whether this ripple is currently animating.
304         */
305        public boolean isRunning() {
306            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
307            return mEnterTime >= 0 && mEnterTime <= currentTime
308                    && (mExitTime < 0 || currentTime <= mExitTime + EXIT_DURATION);
309        }
310
311        public void update() {
312            // Track three states:
313            // - Enter: touch begins, affects outer radius
314            // - Outside: touch moves outside bounds, affects maximum outer radius
315            // - Exit: touch ends, affects inner radius
316            final long currentTime = AnimationUtils.currentAnimationTimeMillis();
317            mTarget.mEnterState = mEnterTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
318                    MathUtils.constrain((currentTime - mEnterTime) / (float) ENTER_DURATION, 0, 1));
319            mTarget.mExitState = mExitTime < 0 ? 0 : INTERPOLATOR.getInterpolation(
320                    MathUtils.constrain((currentTime - mExitTime) / (float) EXIT_DURATION, 0, 1));
321            mTarget.mOutsideState = mOutsideTime < 0 ? 1 : INTERPOLATOR.getInterpolation(
322                    MathUtils.constrain((currentTime - mOutsideTime) / (float) OUTSIDE_DURATION, 0, 1));
323
324            // Pulse is a little more complicated.
325            if (mTarget.mPulse) {
326                final long pulseTime = (currentTime - mEnterTime - ENTER_DURATION - PULSE_DELAY);
327                mTarget.mPulseState = pulseTime < 0 ? -1
328                        : (pulseTime % (PULSE_INTERVAL + PULSE_DURATION)) / (float) PULSE_DURATION;
329            }
330        }
331    }
332}
333