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
17package android.widget;
18
19import android.graphics.Canvas;
20import android.graphics.drawable.Drawable;
21import android.view.animation.AnimationUtils;
22import android.view.animation.DecelerateInterpolator;
23import android.view.animation.Interpolator;
24
25/**
26 * This class performs the glow effect used at the edges of scrollable widgets.
27 * @hide
28 */
29public class EdgeGlow {
30    private static final String TAG = "EdgeGlow";
31
32    // Time it will take the effect to fully recede in ms
33    private static final int RECEDE_TIME = 1000;
34
35    // Time it will take before a pulled glow begins receding
36    private static final int PULL_TIME = 167;
37
38    // Time it will take for a pulled glow to decay to partial strength before release
39    private static final int PULL_DECAY_TIME = 1000;
40
41    private static final float MAX_ALPHA = 0.8f;
42    private static final float HELD_EDGE_ALPHA = 0.7f;
43    private static final float HELD_EDGE_SCALE_Y = 0.5f;
44    private static final float HELD_GLOW_ALPHA = 0.5f;
45    private static final float HELD_GLOW_SCALE_Y = 0.5f;
46
47    private static final float MAX_GLOW_HEIGHT = 3.f;
48
49    private static final float PULL_GLOW_BEGIN = 1.f;
50    private static final float PULL_EDGE_BEGIN = 0.6f;
51
52    // Minimum velocity that will be absorbed
53    private static final int MIN_VELOCITY = 100;
54
55    private static final float EPSILON = 0.001f;
56
57    private final Drawable mEdge;
58    private final Drawable mGlow;
59    private int mWidth;
60    private int mHeight;
61
62    private float mEdgeAlpha;
63    private float mEdgeScaleY;
64    private float mGlowAlpha;
65    private float mGlowScaleY;
66
67    private float mEdgeAlphaStart;
68    private float mEdgeAlphaFinish;
69    private float mEdgeScaleYStart;
70    private float mEdgeScaleYFinish;
71    private float mGlowAlphaStart;
72    private float mGlowAlphaFinish;
73    private float mGlowScaleYStart;
74    private float mGlowScaleYFinish;
75
76    private long mStartTime;
77    private float mDuration;
78
79    private final Interpolator mInterpolator;
80
81    private static final int STATE_IDLE = 0;
82    private static final int STATE_PULL = 1;
83    private static final int STATE_ABSORB = 2;
84    private static final int STATE_RECEDE = 3;
85    private static final int STATE_PULL_DECAY = 4;
86
87    // How much dragging should effect the height of the edge image.
88    // Number determined by user testing.
89    private static final int PULL_DISTANCE_EDGE_FACTOR = 5;
90
91    // How much dragging should effect the height of the glow image.
92    // Number determined by user testing.
93    private static final int PULL_DISTANCE_GLOW_FACTOR = 5;
94    private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f;
95
96    private static final int VELOCITY_EDGE_FACTOR = 8;
97    private static final int VELOCITY_GLOW_FACTOR = 16;
98
99    private int mState = STATE_IDLE;
100
101    private float mPullDistance;
102
103    public EdgeGlow(Drawable edge, Drawable glow) {
104        mEdge = edge;
105        mGlow = glow;
106
107        mInterpolator = new DecelerateInterpolator();
108    }
109
110    public void setSize(int width, int height) {
111        mWidth = width;
112        mHeight = height;
113    }
114
115    public boolean isFinished() {
116        return mState == STATE_IDLE;
117    }
118
119    public void finish() {
120        mState = STATE_IDLE;
121    }
122
123    /**
124     * Call when the object is pulled by the user.
125     *
126     * @param deltaDistance Change in distance since the last call
127     */
128    public void onPull(float deltaDistance) {
129        final long now = AnimationUtils.currentAnimationTimeMillis();
130        if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
131            return;
132        }
133        if (mState != STATE_PULL) {
134            mGlowScaleY = PULL_GLOW_BEGIN;
135        }
136        mState = STATE_PULL;
137
138        mStartTime = now;
139        mDuration = PULL_TIME;
140
141        mPullDistance += deltaDistance;
142        float distance = Math.abs(mPullDistance);
143
144        mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
145        mEdgeScaleY = mEdgeScaleYStart = Math.max(
146                HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
147
148        mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
149                mGlowAlpha +
150                (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
151
152        float glowChange = Math.abs(deltaDistance);
153        if (deltaDistance > 0 && mPullDistance < 0) {
154            glowChange = -glowChange;
155        }
156        if (mPullDistance == 0) {
157            mGlowScaleY = 0;
158        }
159
160        // Do not allow glow to get larger than MAX_GLOW_HEIGHT.
161        mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max(
162                0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));
163
164        mEdgeAlphaFinish = mEdgeAlpha;
165        mEdgeScaleYFinish = mEdgeScaleY;
166        mGlowAlphaFinish = mGlowAlpha;
167        mGlowScaleYFinish = mGlowScaleY;
168    }
169
170    /**
171     * Call when the object is released after being pulled.
172     */
173    public void onRelease() {
174        mPullDistance = 0;
175
176        if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
177            return;
178        }
179
180        mState = STATE_RECEDE;
181        mEdgeAlphaStart = mEdgeAlpha;
182        mEdgeScaleYStart = mEdgeScaleY;
183        mGlowAlphaStart = mGlowAlpha;
184        mGlowScaleYStart = mGlowScaleY;
185
186        mEdgeAlphaFinish = 0.f;
187        mEdgeScaleYFinish = 0.f;
188        mGlowAlphaFinish = 0.f;
189        mGlowScaleYFinish = 0.f;
190
191        mStartTime = AnimationUtils.currentAnimationTimeMillis();
192        mDuration = RECEDE_TIME;
193    }
194
195    /**
196     * Call when the effect absorbs an impact at the given velocity.
197     *
198     * @param velocity Velocity at impact in pixels per second.
199     */
200    public void onAbsorb(int velocity) {
201        mState = STATE_ABSORB;
202        velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
203
204        mStartTime = AnimationUtils.currentAnimationTimeMillis();
205        mDuration = 0.1f + (velocity * 0.03f);
206
207        // The edge should always be at least partially visible, regardless
208        // of velocity.
209        mEdgeAlphaStart = 0.f;
210        mEdgeScaleY = mEdgeScaleYStart = 0.f;
211        // The glow depends more on the velocity, and therefore starts out
212        // nearly invisible.
213        mGlowAlphaStart = 0.5f;
214        mGlowScaleYStart = 0.f;
215
216        // Factor the velocity by 8. Testing on device shows this works best to
217        // reflect the strength of the user's scrolling.
218        mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
219        // Edge should never get larger than the size of its asset.
220        mEdgeScaleYFinish = Math.max(
221                HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
222
223        // Growth for the size of the glow should be quadratic to properly
224        // respond
225        // to a user's scrolling speed. The faster the scrolling speed, the more
226        // intense the effect should be for both the size and the saturation.
227        mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
228        // Alpha should change for the glow as well as size.
229        mGlowAlphaFinish = Math.max(
230                mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
231    }
232
233
234    /**
235     * Draw into the provided canvas. Assumes that the canvas has been rotated
236     * accordingly and the size has been set. The effect will be drawn the full
237     * width of X=0 to X=width, emitting from Y=0 and extending to some factor <
238     * 1.f of height.
239     *
240     * @param canvas Canvas to draw into
241     * @return true if drawing should continue beyond this frame to continue the
242     *         animation
243     */
244    public boolean draw(Canvas canvas) {
245        update();
246
247        final int edgeHeight = mEdge.getIntrinsicHeight();
248        final int glowHeight = mGlow.getIntrinsicHeight();
249
250        final float distScale = (float) mHeight / mWidth;
251
252        mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
253        // Width of the image should be 3 * the width of the screen.
254        // Should start off screen to the left.
255        mGlow.setBounds(-mWidth, 0, mWidth * 2, (int) Math.min(
256                glowHeight * mGlowScaleY * distScale * 0.6f, mHeight * MAX_GLOW_HEIGHT));
257        mGlow.draw(canvas);
258
259        mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
260        mEdge.setBounds(0, 0, mWidth, (int) (edgeHeight * mEdgeScaleY));
261        mEdge.draw(canvas);
262
263        return mState != STATE_IDLE;
264    }
265
266    private void update() {
267        final long time = AnimationUtils.currentAnimationTimeMillis();
268        final float t = Math.min((time - mStartTime) / mDuration, 1.f);
269
270        final float interp = mInterpolator.getInterpolation(t);
271
272        mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
273        mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
274        mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
275        mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
276
277        if (t >= 1.f - EPSILON) {
278            switch (mState) {
279                case STATE_ABSORB:
280                    mState = STATE_RECEDE;
281                    mStartTime = AnimationUtils.currentAnimationTimeMillis();
282                    mDuration = RECEDE_TIME;
283
284                    mEdgeAlphaStart = mEdgeAlpha;
285                    mEdgeScaleYStart = mEdgeScaleY;
286                    mGlowAlphaStart = mGlowAlpha;
287                    mGlowScaleYStart = mGlowScaleY;
288
289                    // After absorb, the glow and edge should fade to nothing.
290                    mEdgeAlphaFinish = 0.f;
291                    mEdgeScaleYFinish = 0.f;
292                    mGlowAlphaFinish = 0.f;
293                    mGlowScaleYFinish = 0.f;
294                    break;
295                case STATE_PULL:
296                    mState = STATE_PULL_DECAY;
297                    mStartTime = AnimationUtils.currentAnimationTimeMillis();
298                    mDuration = PULL_DECAY_TIME;
299
300                    mEdgeAlphaStart = mEdgeAlpha;
301                    mEdgeScaleYStart = mEdgeScaleY;
302                    mGlowAlphaStart = mGlowAlpha;
303                    mGlowScaleYStart = mGlowScaleY;
304
305                    // After pull, the glow and edge should fade to nothing.
306                    mEdgeAlphaFinish = 0.f;
307                    mEdgeScaleYFinish = 0.f;
308                    mGlowAlphaFinish = 0.f;
309                    mGlowScaleYFinish = 0.f;
310                    break;
311                case STATE_PULL_DECAY:
312                    // When receding, we want edge to decrease more slowly
313                    // than the glow.
314                    float factor = mGlowScaleYFinish != 0 ? 1
315                            / (mGlowScaleYFinish * mGlowScaleYFinish)
316                            : Float.MAX_VALUE;
317                    mEdgeScaleY = mEdgeScaleYStart +
318                        (mEdgeScaleYFinish - mEdgeScaleYStart) *
319                            interp * factor;
320                    break;
321                case STATE_RECEDE:
322                    mState = STATE_IDLE;
323                    break;
324            }
325        }
326    }
327}
328