blob: 30752e00d5425cfb4c3271de8092f84b220e36fa [file] [log] [blame]
Adam Powell637d3372010-08-25 14:37:03 -07001/*
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
Romain Guy9d849a22012-03-14 16:41:42 -070019import android.graphics.Rect;
Adam Powell89935e42011-08-31 14:26:12 -070020import com.android.internal.R;
21
Mindy Pereira4e30d892010-11-24 15:32:39 -080022import android.content.Context;
Adam Powell89935e42011-08-31 14:26:12 -070023import android.content.res.Resources;
Adam Powell637d3372010-08-25 14:37:03 -070024import android.graphics.Canvas;
25import android.graphics.drawable.Drawable;
26import android.view.animation.AnimationUtils;
27import android.view.animation.DecelerateInterpolator;
28import android.view.animation.Interpolator;
29
30/**
Adam Powell89935e42011-08-31 14:26:12 -070031 * This class performs the graphical effect used at the edges of scrollable widgets
32 * when the user scrolls beyond the content bounds in 2D space.
33 *
34 * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
35 * instance for each edge that should show the effect, feed it input data using
36 * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
37 * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
38 * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
39 * false after drawing, the edge effect's animation is not yet complete and the widget
40 * should schedule another drawing pass to continue the animation.</p>
41 *
42 * <p>When drawing, widgets should draw their main content and child views first,
43 * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
44 * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
45 * The edge effect may then be drawn on top of the view's content using the
46 * {@link #draw(Canvas)} method.</p>
Adam Powell637d3372010-08-25 14:37:03 -070047 */
Adam Powell89935e42011-08-31 14:26:12 -070048public class EdgeEffect {
Romain Guy9d849a22012-03-14 16:41:42 -070049 @SuppressWarnings("UnusedDeclaration")
Adam Powell89935e42011-08-31 14:26:12 -070050 private static final String TAG = "EdgeEffect";
Adam Powell637d3372010-08-25 14:37:03 -070051
52 // Time it will take the effect to fully recede in ms
53 private static final int RECEDE_TIME = 1000;
54
Adam Powell89935e42011-08-31 14:26:12 -070055 // Time it will take before a pulled glow begins receding in ms
Adam Powell637d3372010-08-25 14:37:03 -070056 private static final int PULL_TIME = 167;
57
Adam Powell89935e42011-08-31 14:26:12 -070058 // Time it will take in ms for a pulled glow to decay to partial strength before release
Adam Powell637d3372010-08-25 14:37:03 -070059 private static final int PULL_DECAY_TIME = 1000;
60
Adam Powell539ee872012-02-03 19:00:49 -080061 private static final float MAX_ALPHA = 1.f;
Adam Powell637d3372010-08-25 14:37:03 -070062 private static final float HELD_EDGE_SCALE_Y = 0.5f;
Adam Powell637d3372010-08-25 14:37:03 -070063
Mindy Pereira4e30d892010-11-24 15:32:39 -080064 private static final float MAX_GLOW_HEIGHT = 4.f;
Adam Powell637d3372010-08-25 14:37:03 -070065
66 private static final float PULL_GLOW_BEGIN = 1.f;
67 private static final float PULL_EDGE_BEGIN = 0.6f;
68
69 // Minimum velocity that will be absorbed
70 private static final int MIN_VELOCITY = 100;
Christian Robertson2d1acfc2013-09-27 18:54:28 -070071 // Maximum velocity, clamps at this value
72 private static final int MAX_VELOCITY = 10000;
Adam Powell637d3372010-08-25 14:37:03 -070073
74 private static final float EPSILON = 0.001f;
75
76 private final Drawable mEdge;
77 private final Drawable mGlow;
78 private int mWidth;
79 private int mHeight;
Romain Guy9d849a22012-03-14 16:41:42 -070080 private int mX;
81 private int mY;
82 private static final int MIN_WIDTH = 300;
Mindy Pereira4e30d892010-11-24 15:32:39 -080083 private final int mMinWidth;
Adam Powell637d3372010-08-25 14:37:03 -070084
85 private float mEdgeAlpha;
86 private float mEdgeScaleY;
87 private float mGlowAlpha;
88 private float mGlowScaleY;
89
90 private float mEdgeAlphaStart;
91 private float mEdgeAlphaFinish;
92 private float mEdgeScaleYStart;
93 private float mEdgeScaleYFinish;
94 private float mGlowAlphaStart;
95 private float mGlowAlphaFinish;
96 private float mGlowScaleYStart;
97 private float mGlowScaleYFinish;
98
99 private long mStartTime;
100 private float mDuration;
101
102 private final Interpolator mInterpolator;
103
104 private static final int STATE_IDLE = 0;
105 private static final int STATE_PULL = 1;
106 private static final int STATE_ABSORB = 2;
107 private static final int STATE_RECEDE = 3;
108 private static final int STATE_PULL_DECAY = 4;
109
110 // How much dragging should effect the height of the edge image.
111 // Number determined by user testing.
Mindy Pereira4e30d892010-11-24 15:32:39 -0800112 private static final int PULL_DISTANCE_EDGE_FACTOR = 7;
Adam Powell637d3372010-08-25 14:37:03 -0700113
114 // How much dragging should effect the height of the glow image.
115 // Number determined by user testing.
Mindy Pereira4e30d892010-11-24 15:32:39 -0800116 private static final int PULL_DISTANCE_GLOW_FACTOR = 7;
117 private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f;
Adam Powell637d3372010-08-25 14:37:03 -0700118
119 private static final int VELOCITY_EDGE_FACTOR = 8;
Christian Robertson2d1acfc2013-09-27 18:54:28 -0700120 private static final int VELOCITY_GLOW_FACTOR = 12;
Adam Powell637d3372010-08-25 14:37:03 -0700121
122 private int mState = STATE_IDLE;
123
124 private float mPullDistance;
Romain Guy9d849a22012-03-14 16:41:42 -0700125
126 private final Rect mBounds = new Rect();
Adam Powell637d3372010-08-25 14:37:03 -0700127
Romain Guya8bfeaf2012-03-15 13:14:14 -0700128 private final int mEdgeHeight;
129 private final int mGlowHeight;
130 private final int mGlowWidth;
131 private final int mMaxEffectHeight;
132
Adam Powell89935e42011-08-31 14:26:12 -0700133 /**
134 * Construct a new EdgeEffect with a theme appropriate for the provided context.
135 * @param context Context used to provide theming and resource information for the EdgeEffect
136 */
137 public EdgeEffect(Context context) {
138 final Resources res = context.getResources();
139 mEdge = res.getDrawable(R.drawable.overscroll_edge);
140 mGlow = res.getDrawable(R.drawable.overscroll_glow);
Adam Powell637d3372010-08-25 14:37:03 -0700141
Romain Guya8bfeaf2012-03-15 13:14:14 -0700142 mEdgeHeight = mEdge.getIntrinsicHeight();
143 mGlowHeight = mGlow.getIntrinsicHeight();
144 mGlowWidth = mGlow.getIntrinsicWidth();
145
146 mMaxEffectHeight = (int) (Math.min(
147 mGlowHeight * MAX_GLOW_HEIGHT * mGlowHeight / mGlowWidth * 0.6f,
148 mGlowHeight * MAX_GLOW_HEIGHT) + 0.5f);
149
Christopher Tate1373a8e2011-11-10 19:59:13 -0800150 mMinWidth = (int) (res.getDisplayMetrics().density * MIN_WIDTH + 0.5f);
Adam Powell637d3372010-08-25 14:37:03 -0700151 mInterpolator = new DecelerateInterpolator();
152 }
153
Adam Powell89935e42011-08-31 14:26:12 -0700154 /**
155 * Set the size of this edge effect in pixels.
156 *
157 * @param width Effect width in pixels
158 * @param height Effect height in pixels
159 */
Adam Powell637d3372010-08-25 14:37:03 -0700160 public void setSize(int width, int height) {
161 mWidth = width;
162 mHeight = height;
163 }
164
Adam Powell89935e42011-08-31 14:26:12 -0700165 /**
Romain Guy9d849a22012-03-14 16:41:42 -0700166 * Set the position of this edge effect in pixels. This position is
Romain Guya8bfeaf2012-03-15 13:14:14 -0700167 * only used by {@link #getBounds(boolean)}.
Romain Guy9d849a22012-03-14 16:41:42 -0700168 *
169 * @param x The position of the edge effect on the X axis
170 * @param y The position of the edge effect on the Y axis
171 */
172 void setPosition(int x, int y) {
173 mX = x;
174 mY = y;
175 }
176
Romain Guy9d849a22012-03-14 16:41:42 -0700177 /**
Adam Powell89935e42011-08-31 14:26:12 -0700178 * Reports if this EdgeEffect's animation is finished. If this method returns false
179 * after a call to {@link #draw(Canvas)} the host widget should schedule another
180 * drawing pass to continue the animation.
181 *
182 * @return true if animation is finished, false if drawing should continue on the next frame.
183 */
Adam Powell637d3372010-08-25 14:37:03 -0700184 public boolean isFinished() {
185 return mState == STATE_IDLE;
186 }
187
Adam Powell89935e42011-08-31 14:26:12 -0700188 /**
189 * Immediately finish the current animation.
190 * After this call {@link #isFinished()} will return true.
191 */
Adam Powell637d3372010-08-25 14:37:03 -0700192 public void finish() {
193 mState = STATE_IDLE;
194 }
195
196 /**
Adam Powell89935e42011-08-31 14:26:12 -0700197 * A view should call this when content is pulled away from an edge by the user.
198 * This will update the state of the current visual effect and its associated animation.
199 * The host view should always {@link android.view.View#invalidate()} after this
200 * and draw the results accordingly.
Adam Powell637d3372010-08-25 14:37:03 -0700201 *
Adam Powell89935e42011-08-31 14:26:12 -0700202 * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
203 * 1.f (full length of the view) or negative values to express change
204 * back toward the edge reached to initiate the effect.
Adam Powell637d3372010-08-25 14:37:03 -0700205 */
206 public void onPull(float deltaDistance) {
207 final long now = AnimationUtils.currentAnimationTimeMillis();
208 if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
209 return;
210 }
211 if (mState != STATE_PULL) {
212 mGlowScaleY = PULL_GLOW_BEGIN;
213 }
214 mState = STATE_PULL;
215
216 mStartTime = now;
217 mDuration = PULL_TIME;
218
219 mPullDistance += deltaDistance;
220 float distance = Math.abs(mPullDistance);
221
222 mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
223 mEdgeScaleY = mEdgeScaleYStart = Math.max(
224 HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
225
226 mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
227 mGlowAlpha +
228 (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
229
230 float glowChange = Math.abs(deltaDistance);
231 if (deltaDistance > 0 && mPullDistance < 0) {
232 glowChange = -glowChange;
233 }
234 if (mPullDistance == 0) {
235 mGlowScaleY = 0;
236 }
237
238 // Do not allow glow to get larger than MAX_GLOW_HEIGHT.
239 mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max(
240 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));
241
242 mEdgeAlphaFinish = mEdgeAlpha;
243 mEdgeScaleYFinish = mEdgeScaleY;
244 mGlowAlphaFinish = mGlowAlpha;
245 mGlowScaleYFinish = mGlowScaleY;
246 }
247
248 /**
249 * Call when the object is released after being pulled.
Adam Powell89935e42011-08-31 14:26:12 -0700250 * This will begin the "decay" phase of the effect. After calling this method
251 * the host view should {@link android.view.View#invalidate()} and thereby
252 * draw the results accordingly.
Adam Powell637d3372010-08-25 14:37:03 -0700253 */
254 public void onRelease() {
255 mPullDistance = 0;
256
257 if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
258 return;
259 }
260
261 mState = STATE_RECEDE;
262 mEdgeAlphaStart = mEdgeAlpha;
263 mEdgeScaleYStart = mEdgeScaleY;
264 mGlowAlphaStart = mGlowAlpha;
265 mGlowScaleYStart = mGlowScaleY;
266
267 mEdgeAlphaFinish = 0.f;
268 mEdgeScaleYFinish = 0.f;
269 mGlowAlphaFinish = 0.f;
270 mGlowScaleYFinish = 0.f;
271
272 mStartTime = AnimationUtils.currentAnimationTimeMillis();
273 mDuration = RECEDE_TIME;
274 }
275
276 /**
277 * Call when the effect absorbs an impact at the given velocity.
Adam Powell89935e42011-08-31 14:26:12 -0700278 * Used when a fling reaches the scroll boundary.
279 *
280 * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
281 * the method <code>getCurrVelocity</code> will provide a reasonable approximation
282 * to use here.</p>
Adam Powell637d3372010-08-25 14:37:03 -0700283 *
284 * @param velocity Velocity at impact in pixels per second.
285 */
286 public void onAbsorb(int velocity) {
287 mState = STATE_ABSORB;
Christian Robertson2d1acfc2013-09-27 18:54:28 -0700288 velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY);
Adam Powell637d3372010-08-25 14:37:03 -0700289
290 mStartTime = AnimationUtils.currentAnimationTimeMillis();
Christian Robertson2d1acfc2013-09-27 18:54:28 -0700291 mDuration = 0.15f + (velocity * 0.02f);
Adam Powell637d3372010-08-25 14:37:03 -0700292
293 // The edge should always be at least partially visible, regardless
294 // of velocity.
295 mEdgeAlphaStart = 0.f;
296 mEdgeScaleY = mEdgeScaleYStart = 0.f;
297 // The glow depends more on the velocity, and therefore starts out
298 // nearly invisible.
Christian Robertson2d1acfc2013-09-27 18:54:28 -0700299 mGlowAlphaStart = 0.3f;
Adam Powell637d3372010-08-25 14:37:03 -0700300 mGlowScaleYStart = 0.f;
301
302 // Factor the velocity by 8. Testing on device shows this works best to
303 // reflect the strength of the user's scrolling.
304 mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
305 // Edge should never get larger than the size of its asset.
306 mEdgeScaleYFinish = Math.max(
307 HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
308
309 // Growth for the size of the glow should be quadratic to properly
310 // respond
311 // to a user's scrolling speed. The faster the scrolling speed, the more
312 // intense the effect should be for both the size and the saturation.
313 mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
314 // Alpha should change for the glow as well as size.
315 mGlowAlphaFinish = Math.max(
316 mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
317 }
318
319
320 /**
321 * Draw into the provided canvas. Assumes that the canvas has been rotated
322 * accordingly and the size has been set. The effect will be drawn the full
Adam Powell89935e42011-08-31 14:26:12 -0700323 * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
Adam Powell637d3372010-08-25 14:37:03 -0700324 * 1.f of height.
325 *
326 * @param canvas Canvas to draw into
327 * @return true if drawing should continue beyond this frame to continue the
328 * animation
329 */
330 public boolean draw(Canvas canvas) {
331 update();
332
Adam Powell637d3372010-08-25 14:37:03 -0700333 mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
Mindy Pereiraa5531d72010-11-23 11:07:30 -0800334
Mindy Pereira4e30d892010-11-24 15:32:39 -0800335 int glowBottom = (int) Math.min(
Romain Guya8bfeaf2012-03-15 13:14:14 -0700336 mGlowHeight * mGlowScaleY * mGlowHeight / mGlowWidth * 0.6f,
337 mGlowHeight * MAX_GLOW_HEIGHT);
Mindy Pereira4e30d892010-11-24 15:32:39 -0800338 if (mWidth < mMinWidth) {
339 // Center the glow and clip it.
Jim Millerffc41962011-03-02 20:41:46 -0800340 int glowLeft = (mWidth - mMinWidth)/2;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800341 mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom);
342 } else {
343 // Stretch the glow to fit.
344 mGlow.setBounds(0, 0, mWidth, glowBottom);
345 }
346
Adam Powell637d3372010-08-25 14:37:03 -0700347 mGlow.draw(canvas);
348
349 mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
Mindy Pereiraa5531d72010-11-23 11:07:30 -0800350
Romain Guya8bfeaf2012-03-15 13:14:14 -0700351 int edgeBottom = (int) (mEdgeHeight * mEdgeScaleY);
Mindy Pereira4e30d892010-11-24 15:32:39 -0800352 if (mWidth < mMinWidth) {
353 // Center the edge and clip it.
Jim Millerffc41962011-03-02 20:41:46 -0800354 int edgeLeft = (mWidth - mMinWidth)/2;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800355 mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom);
356 } else {
357 // Stretch the edge to fit.
358 mEdge.setBounds(0, 0, mWidth, edgeBottom);
359 }
Adam Powell637d3372010-08-25 14:37:03 -0700360 mEdge.draw(canvas);
361
Romain Guy9d849a22012-03-14 16:41:42 -0700362 if (mState == STATE_RECEDE && glowBottom == 0 && edgeBottom == 0) {
363 mState = STATE_IDLE;
364 }
365
Adam Powell637d3372010-08-25 14:37:03 -0700366 return mState != STATE_IDLE;
367 }
368
Romain Guy9d849a22012-03-14 16:41:42 -0700369 /**
370 * Returns the bounds of the edge effect.
Romain Guya8bfeaf2012-03-15 13:14:14 -0700371 *
372 * @hide
Romain Guy9d849a22012-03-14 16:41:42 -0700373 */
Romain Guya8bfeaf2012-03-15 13:14:14 -0700374 public Rect getBounds(boolean reverse) {
375 mBounds.set(0, 0, mWidth, mMaxEffectHeight);
376 mBounds.offset(mX, mY - (reverse ? mMaxEffectHeight : 0));
377
Romain Guy9d849a22012-03-14 16:41:42 -0700378 return mBounds;
379 }
380
Adam Powell637d3372010-08-25 14:37:03 -0700381 private void update() {
382 final long time = AnimationUtils.currentAnimationTimeMillis();
383 final float t = Math.min((time - mStartTime) / mDuration, 1.f);
384
385 final float interp = mInterpolator.getInterpolation(t);
386
387 mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
388 mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
389 mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
390 mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
391
392 if (t >= 1.f - EPSILON) {
393 switch (mState) {
394 case STATE_ABSORB:
395 mState = STATE_RECEDE;
396 mStartTime = AnimationUtils.currentAnimationTimeMillis();
397 mDuration = RECEDE_TIME;
398
399 mEdgeAlphaStart = mEdgeAlpha;
400 mEdgeScaleYStart = mEdgeScaleY;
401 mGlowAlphaStart = mGlowAlpha;
402 mGlowScaleYStart = mGlowScaleY;
403
404 // After absorb, the glow and edge should fade to nothing.
405 mEdgeAlphaFinish = 0.f;
406 mEdgeScaleYFinish = 0.f;
407 mGlowAlphaFinish = 0.f;
408 mGlowScaleYFinish = 0.f;
409 break;
410 case STATE_PULL:
411 mState = STATE_PULL_DECAY;
412 mStartTime = AnimationUtils.currentAnimationTimeMillis();
413 mDuration = PULL_DECAY_TIME;
414
415 mEdgeAlphaStart = mEdgeAlpha;
416 mEdgeScaleYStart = mEdgeScaleY;
417 mGlowAlphaStart = mGlowAlpha;
418 mGlowScaleYStart = mGlowScaleY;
419
420 // After pull, the glow and edge should fade to nothing.
421 mEdgeAlphaFinish = 0.f;
422 mEdgeScaleYFinish = 0.f;
423 mGlowAlphaFinish = 0.f;
424 mGlowScaleYFinish = 0.f;
425 break;
426 case STATE_PULL_DECAY:
427 // When receding, we want edge to decrease more slowly
428 // than the glow.
429 float factor = mGlowScaleYFinish != 0 ? 1
430 / (mGlowScaleYFinish * mGlowScaleYFinish)
431 : Float.MAX_VALUE;
432 mEdgeScaleY = mEdgeScaleYStart +
433 (mEdgeScaleYFinish - mEdgeScaleYStart) *
434 interp * factor;
Daniel Mladenovic0b1ab3a2011-02-21 09:17:40 +0100435 mState = STATE_RECEDE;
Adam Powell637d3372010-08-25 14:37:03 -0700436 break;
437 case STATE_RECEDE:
438 mState = STATE_IDLE;
439 break;
440 }
441 }
442 }
443}