• 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 
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      */
EdgeEffect(Context context)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      */
setSize(int width, int height)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      */
setPosition(int x, int y)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      */
isFinished()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      */
finish()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      */
onPull(float deltaDistance)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      */
onRelease()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      */
onAbsorb(int velocity)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      */
draw(Canvas canvas)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      */
getBounds(boolean reverse)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 
update()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