• 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.annotation.ColorInt;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.graphics.Paint;
24 import android.graphics.PorterDuff;
25 import android.graphics.PorterDuffXfermode;
26 import android.graphics.Rect;
27 import android.view.animation.AnimationUtils;
28 import android.view.animation.DecelerateInterpolator;
29 import android.view.animation.Interpolator;
30 
31 /**
32  * This class performs the graphical effect used at the edges of scrollable widgets
33  * when the user scrolls beyond the content bounds in 2D space.
34  *
35  * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
36  * instance for each edge that should show the effect, feed it input data using
37  * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
38  * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
39  * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
40  * false after drawing, the edge effect's animation is not yet complete and the widget
41  * should schedule another drawing pass to continue the animation.</p>
42  *
43  * <p>When drawing, widgets should draw their main content and child views first,
44  * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
45  * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
46  * The edge effect may then be drawn on top of the view's content using the
47  * {@link #draw(Canvas)} method.</p>
48  */
49 public class EdgeEffect {
50     @SuppressWarnings("UnusedDeclaration")
51     private static final String TAG = "EdgeEffect";
52 
53     // Time it will take the effect to fully recede in ms
54     private static final int RECEDE_TIME = 600;
55 
56     // Time it will take before a pulled glow begins receding in ms
57     private static final int PULL_TIME = 167;
58 
59     // Time it will take in ms for a pulled glow to decay to partial strength before release
60     private static final int PULL_DECAY_TIME = 2000;
61 
62     private static final float MAX_ALPHA = 0.15f;
63     private static final float GLOW_ALPHA_START = .09f;
64 
65     private static final float MAX_GLOW_SCALE = 2.f;
66 
67     private static final float PULL_GLOW_BEGIN = 0.f;
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 static final double ANGLE = Math.PI / 6;
77     private static final float SIN = (float) Math.sin(ANGLE);
78     private static final float COS = (float) Math.cos(ANGLE);
79     private static final float RADIUS_FACTOR = 0.6f;
80 
81     private float mGlowAlpha;
82     private float mGlowScaleY;
83 
84     private float mGlowAlphaStart;
85     private float mGlowAlphaFinish;
86     private float mGlowScaleYStart;
87     private float mGlowScaleYFinish;
88 
89     private long mStartTime;
90     private float mDuration;
91 
92     private final Interpolator mInterpolator;
93 
94     private static final int STATE_IDLE = 0;
95     private static final int STATE_PULL = 1;
96     private static final int STATE_ABSORB = 2;
97     private static final int STATE_RECEDE = 3;
98     private static final int STATE_PULL_DECAY = 4;
99 
100     private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f;
101 
102     private static final int VELOCITY_GLOW_FACTOR = 6;
103 
104     private int mState = STATE_IDLE;
105 
106     private float mPullDistance;
107 
108     private final Rect mBounds = new Rect();
109     private final Paint mPaint = new Paint();
110     private float mRadius;
111     private float mBaseGlowScale;
112     private float mDisplacement = 0.5f;
113     private float mTargetDisplacement = 0.5f;
114 
115     /**
116      * Construct a new EdgeEffect with a theme appropriate for the provided context.
117      * @param context Context used to provide theming and resource information for the EdgeEffect
118      */
EdgeEffect(Context context)119     public EdgeEffect(Context context) {
120         mPaint.setAntiAlias(true);
121         final TypedArray a = context.obtainStyledAttributes(
122                 com.android.internal.R.styleable.EdgeEffect);
123         final int themeColor = a.getColor(
124                 com.android.internal.R.styleable.EdgeEffect_colorEdgeEffect, 0xff666666);
125         a.recycle();
126         mPaint.setColor((themeColor & 0xffffff) | 0x33000000);
127         mPaint.setStyle(Paint.Style.FILL);
128         mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
129         mInterpolator = new DecelerateInterpolator();
130     }
131 
132     /**
133      * Set the size of this edge effect in pixels.
134      *
135      * @param width Effect width in pixels
136      * @param height Effect height in pixels
137      */
setSize(int width, int height)138     public void setSize(int width, int height) {
139         final float r = width * RADIUS_FACTOR / SIN;
140         final float y = COS * r;
141         final float h = r - y;
142         final float or = height * RADIUS_FACTOR / SIN;
143         final float oy = COS * or;
144         final float oh = or - oy;
145 
146         mRadius = r;
147         mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f;
148 
149         mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h));
150     }
151 
152     /**
153      * Reports if this EdgeEffect's animation is finished. If this method returns false
154      * after a call to {@link #draw(Canvas)} the host widget should schedule another
155      * drawing pass to continue the animation.
156      *
157      * @return true if animation is finished, false if drawing should continue on the next frame.
158      */
isFinished()159     public boolean isFinished() {
160         return mState == STATE_IDLE;
161     }
162 
163     /**
164      * Immediately finish the current animation.
165      * After this call {@link #isFinished()} will return true.
166      */
finish()167     public void finish() {
168         mState = STATE_IDLE;
169     }
170 
171     /**
172      * A view should call this when content is pulled away from an edge by the user.
173      * This will update the state of the current visual effect and its associated animation.
174      * The host view should always {@link android.view.View#invalidate()} after this
175      * and draw the results accordingly.
176      *
177      * <p>Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement
178      * of the pull point is known.</p>
179      *
180      * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
181      *                      1.f (full length of the view) or negative values to express change
182      *                      back toward the edge reached to initiate the effect.
183      */
onPull(float deltaDistance)184     public void onPull(float deltaDistance) {
185         onPull(deltaDistance, 0.5f);
186     }
187 
188     /**
189      * A view should call this when content is pulled away from an edge by the user.
190      * This will update the state of the current visual effect and its associated animation.
191      * The host view should always {@link android.view.View#invalidate()} after this
192      * and draw the results accordingly.
193      *
194      * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
195      *                      1.f (full length of the view) or negative values to express change
196      *                      back toward the edge reached to initiate the effect.
197      * @param displacement The displacement from the starting side of the effect of the point
198      *                     initiating the pull. In the case of touch this is the finger position.
199      *                     Values may be from 0-1.
200      */
onPull(float deltaDistance, float displacement)201     public void onPull(float deltaDistance, float displacement) {
202         final long now = AnimationUtils.currentAnimationTimeMillis();
203         mTargetDisplacement = displacement;
204         if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
205             return;
206         }
207         if (mState != STATE_PULL) {
208             mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY);
209         }
210         mState = STATE_PULL;
211 
212         mStartTime = now;
213         mDuration = PULL_TIME;
214 
215         mPullDistance += deltaDistance;
216 
217         final float absdd = Math.abs(deltaDistance);
218         mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
219                 mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
220 
221         if (mPullDistance == 0) {
222             mGlowScaleY = mGlowScaleYStart = 0;
223         } else {
224             final float scale = (float) (Math.max(0, 1 - 1 /
225                     Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3d) / 0.7d);
226 
227             mGlowScaleY = mGlowScaleYStart = scale;
228         }
229 
230         mGlowAlphaFinish = mGlowAlpha;
231         mGlowScaleYFinish = mGlowScaleY;
232     }
233 
234     /**
235      * Call when the object is released after being pulled.
236      * This will begin the "decay" phase of the effect. After calling this method
237      * the host view should {@link android.view.View#invalidate()} and thereby
238      * draw the results accordingly.
239      */
onRelease()240     public void onRelease() {
241         mPullDistance = 0;
242 
243         if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
244             return;
245         }
246 
247         mState = STATE_RECEDE;
248         mGlowAlphaStart = mGlowAlpha;
249         mGlowScaleYStart = mGlowScaleY;
250 
251         mGlowAlphaFinish = 0.f;
252         mGlowScaleYFinish = 0.f;
253 
254         mStartTime = AnimationUtils.currentAnimationTimeMillis();
255         mDuration = RECEDE_TIME;
256     }
257 
258     /**
259      * Call when the effect absorbs an impact at the given velocity.
260      * Used when a fling reaches the scroll boundary.
261      *
262      * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
263      * the method <code>getCurrVelocity</code> will provide a reasonable approximation
264      * to use here.</p>
265      *
266      * @param velocity Velocity at impact in pixels per second.
267      */
onAbsorb(int velocity)268     public void onAbsorb(int velocity) {
269         mState = STATE_ABSORB;
270         velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY);
271 
272         mStartTime = AnimationUtils.currentAnimationTimeMillis();
273         mDuration = 0.15f + (velocity * 0.02f);
274 
275         // The glow depends more on the velocity, and therefore starts out
276         // nearly invisible.
277         mGlowAlphaStart = GLOW_ALPHA_START;
278         mGlowScaleYStart = Math.max(mGlowScaleY, 0.f);
279 
280 
281         // Growth for the size of the glow should be quadratic to properly
282         // respond
283         // to a user's scrolling speed. The faster the scrolling speed, the more
284         // intense the effect should be for both the size and the saturation.
285         mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 1.f);
286         // Alpha should change for the glow as well as size.
287         mGlowAlphaFinish = Math.max(
288                 mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
289         mTargetDisplacement = 0.5f;
290     }
291 
292     /**
293      * Set the color of this edge effect in argb.
294      *
295      * @param color Color in argb
296      */
setColor(@olorInt int color)297     public void setColor(@ColorInt int color) {
298         mPaint.setColor(color);
299     }
300 
301     /**
302      * Return the color of this edge effect in argb.
303      * @return The color of this edge effect in argb
304      */
305     @ColorInt
getColor()306     public int getColor() {
307         return mPaint.getColor();
308     }
309 
310     /**
311      * Draw into the provided canvas. Assumes that the canvas has been rotated
312      * accordingly and the size has been set. The effect will be drawn the full
313      * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
314      * 1.f of height.
315      *
316      * @param canvas Canvas to draw into
317      * @return true if drawing should continue beyond this frame to continue the
318      *         animation
319      */
draw(Canvas canvas)320     public boolean draw(Canvas canvas) {
321         update();
322 
323         final int count = canvas.save();
324 
325         final float centerX = mBounds.centerX();
326         final float centerY = mBounds.height() - mRadius;
327 
328         canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0);
329 
330         final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f;
331         float translateX = mBounds.width() * displacement / 2;
332 
333         canvas.clipRect(mBounds);
334         canvas.translate(translateX, 0);
335         mPaint.setAlpha((int) (0xff * mGlowAlpha));
336         canvas.drawCircle(centerX, centerY, mRadius, mPaint);
337         canvas.restoreToCount(count);
338 
339         boolean oneLastFrame = false;
340         if (mState == STATE_RECEDE && mGlowScaleY == 0) {
341             mState = STATE_IDLE;
342             oneLastFrame = true;
343         }
344 
345         return mState != STATE_IDLE || oneLastFrame;
346     }
347 
348     /**
349      * Return the maximum height that the edge effect will be drawn at given the original
350      * {@link #setSize(int, int) input size}.
351      * @return The maximum height of the edge effect
352      */
getMaxHeight()353     public int getMaxHeight() {
354         return (int) (mBounds.height() * MAX_GLOW_SCALE + 0.5f);
355     }
356 
update()357     private void update() {
358         final long time = AnimationUtils.currentAnimationTimeMillis();
359         final float t = Math.min((time - mStartTime) / mDuration, 1.f);
360 
361         final float interp = mInterpolator.getInterpolation(t);
362 
363         mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
364         mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
365         mDisplacement = (mDisplacement + mTargetDisplacement) / 2;
366 
367         if (t >= 1.f - EPSILON) {
368             switch (mState) {
369                 case STATE_ABSORB:
370                     mState = STATE_RECEDE;
371                     mStartTime = AnimationUtils.currentAnimationTimeMillis();
372                     mDuration = RECEDE_TIME;
373 
374                     mGlowAlphaStart = mGlowAlpha;
375                     mGlowScaleYStart = mGlowScaleY;
376 
377                     // After absorb, the glow should fade to nothing.
378                     mGlowAlphaFinish = 0.f;
379                     mGlowScaleYFinish = 0.f;
380                     break;
381                 case STATE_PULL:
382                     mState = STATE_PULL_DECAY;
383                     mStartTime = AnimationUtils.currentAnimationTimeMillis();
384                     mDuration = PULL_DECAY_TIME;
385 
386                     mGlowAlphaStart = mGlowAlpha;
387                     mGlowScaleYStart = mGlowScaleY;
388 
389                     // After pull, the glow should fade to nothing.
390                     mGlowAlphaFinish = 0.f;
391                     mGlowScaleYFinish = 0.f;
392                     break;
393                 case STATE_PULL_DECAY:
394                     mState = STATE_RECEDE;
395                     break;
396                 case STATE_RECEDE:
397                     mState = STATE_IDLE;
398                     break;
399             }
400         }
401     }
402 }
403