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.Canvas; 20 import android.graphics.drawable.Drawable; 21 import android.view.animation.AnimationUtils; 22 import android.view.animation.DecelerateInterpolator; 23 import android.view.animation.Interpolator; 24 25 /** 26 * This class performs the glow effect used at the edges of scrollable widgets. 27 * @hide 28 */ 29 public class EdgeGlow { 30 private static final String TAG = "EdgeGlow"; 31 32 // Time it will take the effect to fully recede in ms 33 private static final int RECEDE_TIME = 1000; 34 35 // Time it will take before a pulled glow begins receding 36 private static final int PULL_TIME = 167; 37 38 // Time it will take for a pulled glow to decay to partial strength before release 39 private static final int PULL_DECAY_TIME = 1000; 40 41 private static final float MAX_ALPHA = 0.8f; 42 private static final float HELD_EDGE_ALPHA = 0.7f; 43 private static final float HELD_EDGE_SCALE_Y = 0.5f; 44 private static final float HELD_GLOW_ALPHA = 0.5f; 45 private static final float HELD_GLOW_SCALE_Y = 0.5f; 46 47 private static final float MAX_GLOW_HEIGHT = 3.f; 48 49 private static final float PULL_GLOW_BEGIN = 1.f; 50 private static final float PULL_EDGE_BEGIN = 0.6f; 51 52 // Minimum velocity that will be absorbed 53 private static final int MIN_VELOCITY = 100; 54 55 private static final float EPSILON = 0.001f; 56 57 private final Drawable mEdge; 58 private final Drawable mGlow; 59 private int mWidth; 60 private int mHeight; 61 62 private float mEdgeAlpha; 63 private float mEdgeScaleY; 64 private float mGlowAlpha; 65 private float mGlowScaleY; 66 67 private float mEdgeAlphaStart; 68 private float mEdgeAlphaFinish; 69 private float mEdgeScaleYStart; 70 private float mEdgeScaleYFinish; 71 private float mGlowAlphaStart; 72 private float mGlowAlphaFinish; 73 private float mGlowScaleYStart; 74 private float mGlowScaleYFinish; 75 76 private long mStartTime; 77 private float mDuration; 78 79 private final Interpolator mInterpolator; 80 81 private static final int STATE_IDLE = 0; 82 private static final int STATE_PULL = 1; 83 private static final int STATE_ABSORB = 2; 84 private static final int STATE_RECEDE = 3; 85 private static final int STATE_PULL_DECAY = 4; 86 87 // How much dragging should effect the height of the edge image. 88 // Number determined by user testing. 89 private static final int PULL_DISTANCE_EDGE_FACTOR = 5; 90 91 // How much dragging should effect the height of the glow image. 92 // Number determined by user testing. 93 private static final int PULL_DISTANCE_GLOW_FACTOR = 5; 94 private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f; 95 96 private static final int VELOCITY_EDGE_FACTOR = 8; 97 private static final int VELOCITY_GLOW_FACTOR = 16; 98 99 private int mState = STATE_IDLE; 100 101 private float mPullDistance; 102 103 public EdgeGlow(Drawable edge, Drawable glow) { 104 mEdge = edge; 105 mGlow = glow; 106 107 mInterpolator = new DecelerateInterpolator(); 108 } 109 110 public void setSize(int width, int height) { 111 mWidth = width; 112 mHeight = height; 113 } 114 115 public boolean isFinished() { 116 return mState == STATE_IDLE; 117 } 118 119 public void finish() { 120 mState = STATE_IDLE; 121 } 122 123 /** 124 * Call when the object is pulled by the user. 125 * 126 * @param deltaDistance Change in distance since the last call 127 */ 128 public void onPull(float deltaDistance) { 129 final long now = AnimationUtils.currentAnimationTimeMillis(); 130 if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { 131 return; 132 } 133 if (mState != STATE_PULL) { 134 mGlowScaleY = PULL_GLOW_BEGIN; 135 } 136 mState = STATE_PULL; 137 138 mStartTime = now; 139 mDuration = PULL_TIME; 140 141 mPullDistance += deltaDistance; 142 float distance = Math.abs(mPullDistance); 143 144 mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA)); 145 mEdgeScaleY = mEdgeScaleYStart = Math.max( 146 HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f)); 147 148 mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, 149 mGlowAlpha + 150 (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); 151 152 float glowChange = Math.abs(deltaDistance); 153 if (deltaDistance > 0 && mPullDistance < 0) { 154 glowChange = -glowChange; 155 } 156 if (mPullDistance == 0) { 157 mGlowScaleY = 0; 158 } 159 160 // Do not allow glow to get larger than MAX_GLOW_HEIGHT. 161 mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max( 162 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR)); 163 164 mEdgeAlphaFinish = mEdgeAlpha; 165 mEdgeScaleYFinish = mEdgeScaleY; 166 mGlowAlphaFinish = mGlowAlpha; 167 mGlowScaleYFinish = mGlowScaleY; 168 } 169 170 /** 171 * Call when the object is released after being pulled. 172 */ 173 public void onRelease() { 174 mPullDistance = 0; 175 176 if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { 177 return; 178 } 179 180 mState = STATE_RECEDE; 181 mEdgeAlphaStart = mEdgeAlpha; 182 mEdgeScaleYStart = mEdgeScaleY; 183 mGlowAlphaStart = mGlowAlpha; 184 mGlowScaleYStart = mGlowScaleY; 185 186 mEdgeAlphaFinish = 0.f; 187 mEdgeScaleYFinish = 0.f; 188 mGlowAlphaFinish = 0.f; 189 mGlowScaleYFinish = 0.f; 190 191 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 192 mDuration = RECEDE_TIME; 193 } 194 195 /** 196 * Call when the effect absorbs an impact at the given velocity. 197 * 198 * @param velocity Velocity at impact in pixels per second. 199 */ 200 public void onAbsorb(int velocity) { 201 mState = STATE_ABSORB; 202 velocity = Math.max(MIN_VELOCITY, Math.abs(velocity)); 203 204 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 205 mDuration = 0.1f + (velocity * 0.03f); 206 207 // The edge should always be at least partially visible, regardless 208 // of velocity. 209 mEdgeAlphaStart = 0.f; 210 mEdgeScaleY = mEdgeScaleYStart = 0.f; 211 // The glow depends more on the velocity, and therefore starts out 212 // nearly invisible. 213 mGlowAlphaStart = 0.5f; 214 mGlowScaleYStart = 0.f; 215 216 // Factor the velocity by 8. Testing on device shows this works best to 217 // reflect the strength of the user's scrolling. 218 mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1)); 219 // Edge should never get larger than the size of its asset. 220 mEdgeScaleYFinish = Math.max( 221 HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f)); 222 223 // Growth for the size of the glow should be quadratic to properly 224 // respond 225 // to a user's scrolling speed. The faster the scrolling speed, the more 226 // intense the effect should be for both the size and the saturation. 227 mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f); 228 // Alpha should change for the glow as well as size. 229 mGlowAlphaFinish = Math.max( 230 mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); 231 } 232 233 234 /** 235 * Draw into the provided canvas. Assumes that the canvas has been rotated 236 * accordingly and the size has been set. The effect will be drawn the full 237 * width of X=0 to X=width, emitting from Y=0 and extending to some factor < 238 * 1.f of height. 239 * 240 * @param canvas Canvas to draw into 241 * @return true if drawing should continue beyond this frame to continue the 242 * animation 243 */ 244 public boolean draw(Canvas canvas) { 245 update(); 246 247 final int edgeHeight = mEdge.getIntrinsicHeight(); 248 final int glowHeight = mGlow.getIntrinsicHeight(); 249 250 final float distScale = (float) mHeight / mWidth; 251 252 mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255)); 253 // Width of the image should be 3 * the width of the screen. 254 // Should start off screen to the left. 255 mGlow.setBounds(-mWidth, 0, mWidth * 2, (int) Math.min( 256 glowHeight * mGlowScaleY * distScale * 0.6f, mHeight * MAX_GLOW_HEIGHT)); 257 mGlow.draw(canvas); 258 259 mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255)); 260 mEdge.setBounds(0, 0, mWidth, (int) (edgeHeight * mEdgeScaleY)); 261 mEdge.draw(canvas); 262 263 return mState != STATE_IDLE; 264 } 265 266 private void update() { 267 final long time = AnimationUtils.currentAnimationTimeMillis(); 268 final float t = Math.min((time - mStartTime) / mDuration, 1.f); 269 270 final float interp = mInterpolator.getInterpolation(t); 271 272 mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp; 273 mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp; 274 mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; 275 mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; 276 277 if (t >= 1.f - EPSILON) { 278 switch (mState) { 279 case STATE_ABSORB: 280 mState = STATE_RECEDE; 281 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 282 mDuration = RECEDE_TIME; 283 284 mEdgeAlphaStart = mEdgeAlpha; 285 mEdgeScaleYStart = mEdgeScaleY; 286 mGlowAlphaStart = mGlowAlpha; 287 mGlowScaleYStart = mGlowScaleY; 288 289 // After absorb, the glow and edge should fade to nothing. 290 mEdgeAlphaFinish = 0.f; 291 mEdgeScaleYFinish = 0.f; 292 mGlowAlphaFinish = 0.f; 293 mGlowScaleYFinish = 0.f; 294 break; 295 case STATE_PULL: 296 mState = STATE_PULL_DECAY; 297 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 298 mDuration = PULL_DECAY_TIME; 299 300 mEdgeAlphaStart = mEdgeAlpha; 301 mEdgeScaleYStart = mEdgeScaleY; 302 mGlowAlphaStart = mGlowAlpha; 303 mGlowScaleYStart = mGlowScaleY; 304 305 // After pull, the glow and edge should fade to nothing. 306 mEdgeAlphaFinish = 0.f; 307 mEdgeScaleYFinish = 0.f; 308 mGlowAlphaFinish = 0.f; 309 mGlowScaleYFinish = 0.f; 310 break; 311 case STATE_PULL_DECAY: 312 // When receding, we want edge to decrease more slowly 313 // than the glow. 314 float factor = mGlowScaleYFinish != 0 ? 1 315 / (mGlowScaleYFinish * mGlowScaleYFinish) 316 : Float.MAX_VALUE; 317 mEdgeScaleY = mEdgeScaleYStart + 318 (mEdgeScaleYFinish - mEdgeScaleYStart) * 319 interp * factor; 320 break; 321 case STATE_RECEDE: 322 mState = STATE_IDLE; 323 break; 324 } 325 } 326 } 327 } 328