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