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