• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.widget;
18 
19 import android.graphics.Rect;
20 import com.android.internal.R;
21 
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.graphics.Canvas;
25 import android.graphics.drawable.Drawable;
26 import android.view.animation.AnimationUtils;
27 import android.view.animation.DecelerateInterpolator;
28 import 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  */
48 public 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      */
EdgeEffect(Context context)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      */
setSize(int width, int height)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      */
setPosition(int x, int y)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      */
isFinished()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      */
finish()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      */
onPull(float deltaDistance)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      */
onRelease()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      */
onAbsorb(int velocity)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      */
draw(Canvas canvas)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      */
getBounds(boolean reverse)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 
update()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