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 17 package android.widget; 18 19 import android.graphics.Rect; 20 import com.android.internal.R; 21 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.graphics.Canvas; 25 import android.graphics.drawable.Drawable; 26 import android.view.animation.AnimationUtils; 27 import android.view.animation.DecelerateInterpolator; 28 import android.view.animation.Interpolator; 29 30 /** 31 * 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> 47 */ 48 public class EdgeEffect { 49 @SuppressWarnings("UnusedDeclaration") 50 private static final String TAG = "EdgeEffect"; 51 52 // Time it will take the effect to fully recede in ms 53 private static final int RECEDE_TIME = 1000; 54 55 // Time it will take before a pulled glow begins receding in ms 56 private static final int PULL_TIME = 167; 57 58 // Time it will take in ms for a pulled glow to decay to partial strength before release 59 private static final int PULL_DECAY_TIME = 1000; 60 61 private static final float MAX_ALPHA = 1.f; 62 private static final float HELD_EDGE_SCALE_Y = 0.5f; 63 64 private static final float MAX_GLOW_HEIGHT = 4.f; 65 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; 71 // Maximum velocity, clamps at this value 72 private static final int MAX_VELOCITY = 10000; 73 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; 80 private int mX; 81 private int mY; 82 private static final int MIN_WIDTH = 300; 83 private final int mMinWidth; 84 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. 112 private static final int PULL_DISTANCE_EDGE_FACTOR = 7; 113 114 // How much dragging should effect the height of the glow image. 115 // Number determined by user testing. 116 private static final int PULL_DISTANCE_GLOW_FACTOR = 7; 117 private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f; 118 119 private static final int VELOCITY_EDGE_FACTOR = 8; 120 private static final int VELOCITY_GLOW_FACTOR = 12; 121 122 private int mState = STATE_IDLE; 123 124 private float mPullDistance; 125 126 private final Rect mBounds = new Rect(); 127 128 private final int mEdgeHeight; 129 private final int mGlowHeight; 130 private final int mGlowWidth; 131 private final int mMaxEffectHeight; 132 133 /** 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 */ EdgeEffect(Context context)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); 141 142 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 150 mMinWidth = (int) (res.getDisplayMetrics().density * MIN_WIDTH + 0.5f); 151 mInterpolator = new DecelerateInterpolator(); 152 } 153 154 /** 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 */ setSize(int width, int height)160 public void setSize(int width, int height) { 161 mWidth = width; 162 mHeight = height; 163 } 164 165 /** 166 * Set the position of this edge effect in pixels. This position is 167 * only used by {@link #getBounds(boolean)}. 168 * 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 */ setPosition(int x, int y)172 void setPosition(int x, int y) { 173 mX = x; 174 mY = y; 175 } 176 177 /** 178 * 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 */ isFinished()184 public boolean isFinished() { 185 return mState == STATE_IDLE; 186 } 187 188 /** 189 * Immediately finish the current animation. 190 * After this call {@link #isFinished()} will return true. 191 */ finish()192 public void finish() { 193 mState = STATE_IDLE; 194 } 195 196 /** 197 * 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. 201 * 202 * @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. 205 */ onPull(float deltaDistance)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. 250 * 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. 253 */ onRelease()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. 278 * 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> 283 * 284 * @param velocity Velocity at impact in pixels per second. 285 */ onAbsorb(int velocity)286 public void onAbsorb(int velocity) { 287 mState = STATE_ABSORB; 288 velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY); 289 290 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 291 mDuration = 0.15f + (velocity * 0.02f); 292 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. 299 mGlowAlphaStart = 0.3f; 300 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 323 * width of X=0 to X=width, beginning from Y=0 and extending to some factor < 324 * 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 */ draw(Canvas canvas)330 public boolean draw(Canvas canvas) { 331 update(); 332 333 mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255)); 334 335 int glowBottom = (int) Math.min( 336 mGlowHeight * mGlowScaleY * mGlowHeight / mGlowWidth * 0.6f, 337 mGlowHeight * MAX_GLOW_HEIGHT); 338 if (mWidth < mMinWidth) { 339 // Center the glow and clip it. 340 int glowLeft = (mWidth - mMinWidth)/2; 341 mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom); 342 } else { 343 // Stretch the glow to fit. 344 mGlow.setBounds(0, 0, mWidth, glowBottom); 345 } 346 347 mGlow.draw(canvas); 348 349 mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255)); 350 351 int edgeBottom = (int) (mEdgeHeight * mEdgeScaleY); 352 if (mWidth < mMinWidth) { 353 // Center the edge and clip it. 354 int edgeLeft = (mWidth - mMinWidth)/2; 355 mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom); 356 } else { 357 // Stretch the edge to fit. 358 mEdge.setBounds(0, 0, mWidth, edgeBottom); 359 } 360 mEdge.draw(canvas); 361 362 if (mState == STATE_RECEDE && glowBottom == 0 && edgeBottom == 0) { 363 mState = STATE_IDLE; 364 } 365 366 return mState != STATE_IDLE; 367 } 368 369 /** 370 * Returns the bounds of the edge effect. 371 * 372 * @hide 373 */ getBounds(boolean reverse)374 public Rect getBounds(boolean reverse) { 375 mBounds.set(0, 0, mWidth, mMaxEffectHeight); 376 mBounds.offset(mX, mY - (reverse ? mMaxEffectHeight : 0)); 377 378 return mBounds; 379 } 380 update()381 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; 435 mState = STATE_RECEDE; 436 break; 437 case STATE_RECEDE: 438 mState = STATE_IDLE; 439 break; 440 } 441 } 442 } 443 } 444