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