1/*
2 * Copyright (C) 2010 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.widget;
18
19import android.graphics.Rect;
20import com.android.internal.R;
21
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.Canvas;
25import android.graphics.drawable.Drawable;
26import android.view.animation.AnimationUtils;
27import android.view.animation.DecelerateInterpolator;
28import android.view.animation.Interpolator;
29
30/**
31 * This class performs the graphical effect used at the edges of scrollable widgets
32 * when the user scrolls beyond the content bounds in 2D space.
33 *
34 * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
35 * instance for each edge that should show the effect, feed it input data using
36 * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
37 * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
38 * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
39 * false after drawing, the edge effect's animation is not yet complete and the widget
40 * should schedule another drawing pass to continue the animation.</p>
41 *
42 * <p>When drawing, widgets should draw their main content and child views first,
43 * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
44 * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
45 * The edge effect may then be drawn on top of the view's content using the
46 * {@link #draw(Canvas)} method.</p>
47 */
48public class EdgeEffect {
49    @SuppressWarnings("UnusedDeclaration")
50    private static final String TAG = "EdgeEffect";
51
52    // Time it will take the effect to fully recede in ms
53    private static final int RECEDE_TIME = 1000;
54
55    // Time it will take before a pulled glow begins receding in ms
56    private static final int PULL_TIME = 167;
57
58    // Time it will take in ms for a pulled glow to decay to partial strength before release
59    private static final int PULL_DECAY_TIME = 1000;
60
61    private static final float MAX_ALPHA = 1.f;
62    private static final float HELD_EDGE_SCALE_Y = 0.5f;
63
64    private static final float MAX_GLOW_HEIGHT = 4.f;
65
66    private static final float PULL_GLOW_BEGIN = 1.f;
67    private static final float PULL_EDGE_BEGIN = 0.6f;
68
69    // Minimum velocity that will be absorbed
70    private static final int MIN_VELOCITY = 100;
71    // Maximum velocity, clamps at this value
72    private static final int MAX_VELOCITY = 10000;
73
74    private static final float EPSILON = 0.001f;
75
76    private final Drawable mEdge;
77    private final Drawable mGlow;
78    private int mWidth;
79    private int mHeight;
80    private int mX;
81    private int mY;
82    private static final int MIN_WIDTH = 300;
83    private final int mMinWidth;
84
85    private float mEdgeAlpha;
86    private float mEdgeScaleY;
87    private float mGlowAlpha;
88    private float mGlowScaleY;
89
90    private float mEdgeAlphaStart;
91    private float mEdgeAlphaFinish;
92    private float mEdgeScaleYStart;
93    private float mEdgeScaleYFinish;
94    private float mGlowAlphaStart;
95    private float mGlowAlphaFinish;
96    private float mGlowScaleYStart;
97    private float mGlowScaleYFinish;
98
99    private long mStartTime;
100    private float mDuration;
101
102    private final Interpolator mInterpolator;
103
104    private static final int STATE_IDLE = 0;
105    private static final int STATE_PULL = 1;
106    private static final int STATE_ABSORB = 2;
107    private static final int STATE_RECEDE = 3;
108    private static final int STATE_PULL_DECAY = 4;
109
110    // How much dragging should effect the height of the edge image.
111    // Number determined by user testing.
112    private static final int PULL_DISTANCE_EDGE_FACTOR = 7;
113
114    // How much dragging should effect the height of the glow image.
115    // Number determined by user testing.
116    private static final int PULL_DISTANCE_GLOW_FACTOR = 7;
117    private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f;
118
119    private static final int VELOCITY_EDGE_FACTOR = 8;
120    private static final int VELOCITY_GLOW_FACTOR = 12;
121
122    private int mState = STATE_IDLE;
123
124    private float mPullDistance;
125
126    private final Rect mBounds = new Rect();
127
128    private final int mEdgeHeight;
129    private final int mGlowHeight;
130    private final int mGlowWidth;
131    private final int mMaxEffectHeight;
132
133    /**
134     * Construct a new EdgeEffect with a theme appropriate for the provided context.
135     * @param context Context used to provide theming and resource information for the EdgeEffect
136     */
137    public EdgeEffect(Context context) {
138        final Resources res = context.getResources();
139        mEdge = res.getDrawable(R.drawable.overscroll_edge);
140        mGlow = res.getDrawable(R.drawable.overscroll_glow);
141
142        mEdgeHeight = mEdge.getIntrinsicHeight();
143        mGlowHeight = mGlow.getIntrinsicHeight();
144        mGlowWidth = mGlow.getIntrinsicWidth();
145
146        mMaxEffectHeight = (int) (Math.min(
147                mGlowHeight * MAX_GLOW_HEIGHT * mGlowHeight / mGlowWidth * 0.6f,
148                mGlowHeight * MAX_GLOW_HEIGHT) + 0.5f);
149
150        mMinWidth = (int) (res.getDisplayMetrics().density * MIN_WIDTH + 0.5f);
151        mInterpolator = new DecelerateInterpolator();
152    }
153
154    /**
155     * Set the size of this edge effect in pixels.
156     *
157     * @param width Effect width in pixels
158     * @param height Effect height in pixels
159     */
160    public void setSize(int width, int height) {
161        mWidth = width;
162        mHeight = height;
163    }
164
165    /**
166     * Set the position of this edge effect in pixels. This position is
167     * only used by {@link #getBounds(boolean)}.
168     *
169     * @param x The position of the edge effect on the X axis
170     * @param y The position of the edge effect on the Y axis
171     */
172    void setPosition(int x, int y) {
173        mX = x;
174        mY = y;
175    }
176
177    /**
178     * Reports if this EdgeEffect's animation is finished. If this method returns false
179     * after a call to {@link #draw(Canvas)} the host widget should schedule another
180     * drawing pass to continue the animation.
181     *
182     * @return true if animation is finished, false if drawing should continue on the next frame.
183     */
184    public boolean isFinished() {
185        return mState == STATE_IDLE;
186    }
187
188    /**
189     * Immediately finish the current animation.
190     * After this call {@link #isFinished()} will return true.
191     */
192    public void finish() {
193        mState = STATE_IDLE;
194    }
195
196    /**
197     * A view should call this when content is pulled away from an edge by the user.
198     * This will update the state of the current visual effect and its associated animation.
199     * The host view should always {@link android.view.View#invalidate()} after this
200     * and draw the results accordingly.
201     *
202     * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
203     *                      1.f (full length of the view) or negative values to express change
204     *                      back toward the edge reached to initiate the effect.
205     */
206    public void onPull(float deltaDistance) {
207        final long now = AnimationUtils.currentAnimationTimeMillis();
208        if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
209            return;
210        }
211        if (mState != STATE_PULL) {
212            mGlowScaleY = PULL_GLOW_BEGIN;
213        }
214        mState = STATE_PULL;
215
216        mStartTime = now;
217        mDuration = PULL_TIME;
218
219        mPullDistance += deltaDistance;
220        float distance = Math.abs(mPullDistance);
221
222        mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
223        mEdgeScaleY = mEdgeScaleYStart = Math.max(
224                HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
225
226        mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
227                mGlowAlpha +
228                (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
229
230        float glowChange = Math.abs(deltaDistance);
231        if (deltaDistance > 0 && mPullDistance < 0) {
232            glowChange = -glowChange;
233        }
234        if (mPullDistance == 0) {
235            mGlowScaleY = 0;
236        }
237
238        // Do not allow glow to get larger than MAX_GLOW_HEIGHT.
239        mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max(
240                0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));
241
242        mEdgeAlphaFinish = mEdgeAlpha;
243        mEdgeScaleYFinish = mEdgeScaleY;
244        mGlowAlphaFinish = mGlowAlpha;
245        mGlowScaleYFinish = mGlowScaleY;
246    }
247
248    /**
249     * Call when the object is released after being pulled.
250     * This will begin the "decay" phase of the effect. After calling this method
251     * the host view should {@link android.view.View#invalidate()} and thereby
252     * draw the results accordingly.
253     */
254    public void onRelease() {
255        mPullDistance = 0;
256
257        if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
258            return;
259        }
260
261        mState = STATE_RECEDE;
262        mEdgeAlphaStart = mEdgeAlpha;
263        mEdgeScaleYStart = mEdgeScaleY;
264        mGlowAlphaStart = mGlowAlpha;
265        mGlowScaleYStart = mGlowScaleY;
266
267        mEdgeAlphaFinish = 0.f;
268        mEdgeScaleYFinish = 0.f;
269        mGlowAlphaFinish = 0.f;
270        mGlowScaleYFinish = 0.f;
271
272        mStartTime = AnimationUtils.currentAnimationTimeMillis();
273        mDuration = RECEDE_TIME;
274    }
275
276    /**
277     * Call when the effect absorbs an impact at the given velocity.
278     * Used when a fling reaches the scroll boundary.
279     *
280     * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
281     * the method <code>getCurrVelocity</code> will provide a reasonable approximation
282     * to use here.</p>
283     *
284     * @param velocity Velocity at impact in pixels per second.
285     */
286    public void onAbsorb(int velocity) {
287        mState = STATE_ABSORB;
288        velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY);
289
290        mStartTime = AnimationUtils.currentAnimationTimeMillis();
291        mDuration = 0.15f + (velocity * 0.02f);
292
293        // The edge should always be at least partially visible, regardless
294        // of velocity.
295        mEdgeAlphaStart = 0.f;
296        mEdgeScaleY = mEdgeScaleYStart = 0.f;
297        // The glow depends more on the velocity, and therefore starts out
298        // nearly invisible.
299        mGlowAlphaStart = 0.3f;
300        mGlowScaleYStart = 0.f;
301
302        // Factor the velocity by 8. Testing on device shows this works best to
303        // reflect the strength of the user's scrolling.
304        mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
305        // Edge should never get larger than the size of its asset.
306        mEdgeScaleYFinish = Math.max(
307                HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
308
309        // Growth for the size of the glow should be quadratic to properly
310        // respond
311        // to a user's scrolling speed. The faster the scrolling speed, the more
312        // intense the effect should be for both the size and the saturation.
313        mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
314        // Alpha should change for the glow as well as size.
315        mGlowAlphaFinish = Math.max(
316                mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
317    }
318
319
320    /**
321     * Draw into the provided canvas. Assumes that the canvas has been rotated
322     * accordingly and the size has been set. The effect will be drawn the full
323     * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
324     * 1.f of height.
325     *
326     * @param canvas Canvas to draw into
327     * @return true if drawing should continue beyond this frame to continue the
328     *         animation
329     */
330    public boolean draw(Canvas canvas) {
331        update();
332
333        mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
334
335        int glowBottom = (int) Math.min(
336                mGlowHeight * mGlowScaleY * mGlowHeight / mGlowWidth * 0.6f,
337                mGlowHeight * MAX_GLOW_HEIGHT);
338        if (mWidth < mMinWidth) {
339            // Center the glow and clip it.
340            int glowLeft = (mWidth - mMinWidth)/2;
341            mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom);
342        } else {
343            // Stretch the glow to fit.
344            mGlow.setBounds(0, 0, mWidth, glowBottom);
345        }
346
347        mGlow.draw(canvas);
348
349        mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
350
351        int edgeBottom = (int) (mEdgeHeight * mEdgeScaleY);
352        if (mWidth < mMinWidth) {
353            // Center the edge and clip it.
354            int edgeLeft = (mWidth - mMinWidth)/2;
355            mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom);
356        } else {
357            // Stretch the edge to fit.
358            mEdge.setBounds(0, 0, mWidth, edgeBottom);
359        }
360        mEdge.draw(canvas);
361
362        if (mState == STATE_RECEDE && glowBottom == 0 && edgeBottom == 0) {
363            mState = STATE_IDLE;
364        }
365
366        return mState != STATE_IDLE;
367    }
368
369    /**
370     * Returns the bounds of the edge effect.
371     *
372     * @hide
373     */
374    public Rect getBounds(boolean reverse) {
375        mBounds.set(0, 0, mWidth, mMaxEffectHeight);
376        mBounds.offset(mX, mY - (reverse ? mMaxEffectHeight : 0));
377
378        return mBounds;
379    }
380
381    private void update() {
382        final long time = AnimationUtils.currentAnimationTimeMillis();
383        final float t = Math.min((time - mStartTime) / mDuration, 1.f);
384
385        final float interp = mInterpolator.getInterpolation(t);
386
387        mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
388        mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
389        mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
390        mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
391
392        if (t >= 1.f - EPSILON) {
393            switch (mState) {
394                case STATE_ABSORB:
395                    mState = STATE_RECEDE;
396                    mStartTime = AnimationUtils.currentAnimationTimeMillis();
397                    mDuration = RECEDE_TIME;
398
399                    mEdgeAlphaStart = mEdgeAlpha;
400                    mEdgeScaleYStart = mEdgeScaleY;
401                    mGlowAlphaStart = mGlowAlpha;
402                    mGlowScaleYStart = mGlowScaleY;
403
404                    // After absorb, the glow and edge should fade to nothing.
405                    mEdgeAlphaFinish = 0.f;
406                    mEdgeScaleYFinish = 0.f;
407                    mGlowAlphaFinish = 0.f;
408                    mGlowScaleYFinish = 0.f;
409                    break;
410                case STATE_PULL:
411                    mState = STATE_PULL_DECAY;
412                    mStartTime = AnimationUtils.currentAnimationTimeMillis();
413                    mDuration = PULL_DECAY_TIME;
414
415                    mEdgeAlphaStart = mEdgeAlpha;
416                    mEdgeScaleYStart = mEdgeScaleY;
417                    mGlowAlphaStart = mGlowAlpha;
418                    mGlowScaleYStart = mGlowScaleY;
419
420                    // After pull, the glow and edge should fade to nothing.
421                    mEdgeAlphaFinish = 0.f;
422                    mEdgeScaleYFinish = 0.f;
423                    mGlowAlphaFinish = 0.f;
424                    mGlowScaleYFinish = 0.f;
425                    break;
426                case STATE_PULL_DECAY:
427                    // When receding, we want edge to decrease more slowly
428                    // than the glow.
429                    float factor = mGlowScaleYFinish != 0 ? 1
430                            / (mGlowScaleYFinish * mGlowScaleYFinish)
431                            : Float.MAX_VALUE;
432                    mEdgeScaleY = mEdgeScaleYStart +
433                        (mEdgeScaleYFinish - mEdgeScaleYStart) *
434                            interp * factor;
435                    mState = STATE_RECEDE;
436                    break;
437                case STATE_RECEDE:
438                    mState = STATE_IDLE;
439                    break;
440            }
441        }
442    }
443}
444