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