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