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
17package com.android.gallery3d.ui;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Rect;
22import android.view.animation.DecelerateInterpolator;
23import android.view.animation.Interpolator;
24
25import com.android.gallery3d.R;
26import com.android.gallery3d.glrenderer.GLCanvas;
27import 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 */
55public 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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
415        public Drawable(Context context, int resId) {
416            super(context, resId);
417        }
418
419        public int getIntrinsicWidth() {
420            return getWidth();
421        }
422
423        public int getIntrinsicHeight() {
424            return getHeight();
425        }
426
427        public void setBounds(int left, int top, int right, int bottom) {
428            mBounds.set(left, top, right, bottom);
429        }
430
431        public void setAlpha(int alpha) {
432            mAlpha = alpha;
433        }
434
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