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