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