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.animation.ValueAnimator; 20 import android.annotation.ColorInt; 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.compat.Compatibility; 25 import android.compat.annotation.ChangeId; 26 import android.compat.annotation.EnabledSince; 27 import android.compat.annotation.UnsupportedAppUsage; 28 import android.content.Context; 29 import android.content.res.TypedArray; 30 import android.graphics.BlendMode; 31 import android.graphics.Canvas; 32 import android.graphics.Matrix; 33 import android.graphics.Paint; 34 import android.graphics.RecordingCanvas; 35 import android.graphics.Rect; 36 import android.graphics.RenderNode; 37 import android.os.Build; 38 import android.util.AttributeSet; 39 import android.view.animation.AnimationUtils; 40 import android.view.animation.DecelerateInterpolator; 41 import android.view.animation.Interpolator; 42 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 46 /** 47 * This class performs the graphical effect used at the edges of scrollable widgets 48 * when the user scrolls beyond the content bounds in 2D space. 49 * 50 * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an 51 * instance for each edge that should show the effect, feed it input data using 52 * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()}, 53 * and draw the effect using {@link #draw(Canvas)} in the widget's overridden 54 * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns 55 * false after drawing, the edge effect's animation is not yet complete and the widget 56 * should schedule another drawing pass to continue the animation.</p> 57 * 58 * <p>When drawing, widgets should draw their main content and child views first, 59 * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code> 60 * method. (This will invoke onDraw and dispatch drawing to child views as needed.) 61 * The edge effect may then be drawn on top of the view's content using the 62 * {@link #draw(Canvas)} method.</p> 63 */ 64 public class EdgeEffect { 65 /** 66 * This sets the edge effect to use stretch instead of glow. 67 * 68 * @hide 69 */ 70 @ChangeId 71 @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BASE) 72 public static final long USE_STRETCH_EDGE_EFFECT_BY_DEFAULT = 171228096L; 73 74 /** 75 * The default blend mode used by {@link EdgeEffect}. 76 */ 77 public static final BlendMode DEFAULT_BLEND_MODE = BlendMode.SRC_ATOP; 78 79 /** 80 * Completely disable edge effect 81 */ 82 private static final int TYPE_NONE = -1; 83 84 /** 85 * Use a color edge glow for the edge effect. 86 */ 87 private static final int TYPE_GLOW = 0; 88 89 /** 90 * Use a stretch for the edge effect. 91 */ 92 private static final int TYPE_STRETCH = 1; 93 94 /** 95 * The velocity threshold before the spring animation is considered settled. 96 * The idea here is that velocity should be less than 0.1 pixel per second. 97 */ 98 private static final double VELOCITY_THRESHOLD = 0.01; 99 100 /** 101 * The speed at which we should start linearly interpolating to the destination. 102 * When using a spring, as it gets closer to the destination, the speed drops off exponentially. 103 * Instead of landing very slowly, a better experience is achieved if the final 104 * destination is arrived at quicker. 105 */ 106 private static final float LINEAR_VELOCITY_TAKE_OVER = 200f; 107 108 /** 109 * The value threshold before the spring animation is considered close enough to 110 * the destination to be settled. This should be around 0.01 pixel. 111 */ 112 private static final double VALUE_THRESHOLD = 0.001; 113 114 /** 115 * The maximum distance at which we should start linearly interpolating to the destination. 116 * When using a spring, as it gets closer to the destination, the speed drops off exponentially. 117 * Instead of landing very slowly, a better experience is achieved if the final 118 * destination is arrived at quicker. 119 */ 120 private static final double LINEAR_DISTANCE_TAKE_OVER = 8.0; 121 122 /** 123 * The natural frequency of the stretch spring. 124 */ 125 private static final double NATURAL_FREQUENCY = 24.657; 126 127 /** 128 * The damping ratio of the stretch spring. 129 */ 130 private static final double DAMPING_RATIO = 0.98; 131 132 /** 133 * The variation of the velocity for the stretch effect when it meets the bound. 134 * if value is > 1, it will accentuate the absorption of the movement. 135 */ 136 private static final float ON_ABSORB_VELOCITY_ADJUSTMENT = 13f; 137 138 /** @hide */ 139 @IntDef({TYPE_NONE, TYPE_GLOW, TYPE_STRETCH}) 140 @Retention(RetentionPolicy.SOURCE) 141 public @interface EdgeEffectType { 142 } 143 144 private static final float LINEAR_STRETCH_INTENSITY = 0.016f; 145 146 private static final float EXP_STRETCH_INTENSITY = 0.016f; 147 148 private static final float SCROLL_DIST_AFFECTED_BY_EXP_STRETCH = 0.33f; 149 150 @SuppressWarnings("UnusedDeclaration") 151 private static final String TAG = "EdgeEffect"; 152 153 // Time it will take the effect to fully recede in ms 154 private static final int RECEDE_TIME = 600; 155 156 // Time it will take before a pulled glow begins receding in ms 157 private static final int PULL_TIME = 167; 158 159 // Time it will take in ms for a pulled glow to decay to partial strength before release 160 private static final int PULL_DECAY_TIME = 2000; 161 162 private static final float MAX_ALPHA = 0.15f; 163 private static final float GLOW_ALPHA_START = .09f; 164 165 private static final float MAX_GLOW_SCALE = 2.f; 166 167 private static final float PULL_GLOW_BEGIN = 0.f; 168 169 // Minimum velocity that will be absorbed 170 private static final int MIN_VELOCITY = 100; 171 // Maximum velocity, clamps at this value 172 private static final int MAX_VELOCITY = 10000; 173 174 private static final float EPSILON = 0.001f; 175 176 private static final double ANGLE = Math.PI / 6; 177 private static final float SIN = (float) Math.sin(ANGLE); 178 private static final float COS = (float) Math.cos(ANGLE); 179 private static final float RADIUS_FACTOR = 0.6f; 180 181 private float mGlowAlpha; 182 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 183 private float mGlowScaleY; 184 private float mDistance; 185 private float mVelocity; // only for stretch animations 186 187 private float mGlowAlphaStart; 188 private float mGlowAlphaFinish; 189 private float mGlowScaleYStart; 190 private float mGlowScaleYFinish; 191 192 private long mStartTime; 193 private float mDuration; 194 195 private final Interpolator mInterpolator = new DecelerateInterpolator(); 196 197 private static final int STATE_IDLE = 0; 198 private static final int STATE_PULL = 1; 199 private static final int STATE_ABSORB = 2; 200 private static final int STATE_RECEDE = 3; 201 private static final int STATE_PULL_DECAY = 4; 202 203 private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f; 204 205 private static final int VELOCITY_GLOW_FACTOR = 6; 206 207 private int mState = STATE_IDLE; 208 209 private float mPullDistance; 210 211 private final Rect mBounds = new Rect(); 212 private float mWidth; 213 private float mHeight; 214 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769450) 215 private final Paint mPaint = new Paint(); 216 private float mRadius; 217 private float mBaseGlowScale; 218 private float mDisplacement = 0.5f; 219 private float mTargetDisplacement = 0.5f; 220 221 /** 222 * Current edge effect type, consumers should always query 223 * {@link #getCurrentEdgeEffectBehavior()} instead of this parameter 224 * directly in case animations have been disabled (ex. for accessibility reasons) 225 */ 226 private @EdgeEffectType int mEdgeEffectType = TYPE_GLOW; 227 private Matrix mTmpMatrix = null; 228 private float[] mTmpPoints = null; 229 230 /** 231 * Construct a new EdgeEffect with a theme appropriate for the provided context. 232 * @param context Context used to provide theming and resource information for the EdgeEffect 233 */ EdgeEffect(Context context)234 public EdgeEffect(Context context) { 235 this(context, null); 236 } 237 238 /** 239 * Construct a new EdgeEffect with a theme appropriate for the provided context. 240 * @param context Context used to provide theming and resource information for the EdgeEffect 241 * @param attrs The attributes of the XML tag that is inflating the view 242 */ EdgeEffect(@onNull Context context, @Nullable AttributeSet attrs)243 public EdgeEffect(@NonNull Context context, @Nullable AttributeSet attrs) { 244 final TypedArray a = context.obtainStyledAttributes( 245 attrs, com.android.internal.R.styleable.EdgeEffect); 246 final int themeColor = a.getColor( 247 com.android.internal.R.styleable.EdgeEffect_colorEdgeEffect, 0xff666666); 248 mEdgeEffectType = Compatibility.isChangeEnabled(USE_STRETCH_EDGE_EFFECT_BY_DEFAULT) 249 ? TYPE_STRETCH : TYPE_GLOW; 250 a.recycle(); 251 252 mPaint.setAntiAlias(true); 253 mPaint.setColor((themeColor & 0xffffff) | 0x33000000); 254 mPaint.setStyle(Paint.Style.FILL); 255 mPaint.setBlendMode(DEFAULT_BLEND_MODE); 256 } 257 258 @EdgeEffectType getCurrentEdgeEffectBehavior()259 private int getCurrentEdgeEffectBehavior() { 260 if (!ValueAnimator.areAnimatorsEnabled()) { 261 return TYPE_NONE; 262 } else { 263 return mEdgeEffectType; 264 } 265 } 266 267 /** 268 * Set the size of this edge effect in pixels. 269 * 270 * @param width Effect width in pixels 271 * @param height Effect height in pixels 272 */ setSize(int width, int height)273 public void setSize(int width, int height) { 274 final float r = width * RADIUS_FACTOR / SIN; 275 final float y = COS * r; 276 final float h = r - y; 277 final float or = height * RADIUS_FACTOR / SIN; 278 final float oy = COS * or; 279 final float oh = or - oy; 280 281 mRadius = r; 282 mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f; 283 284 mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h)); 285 286 mWidth = width; 287 mHeight = height; 288 } 289 290 /** 291 * Reports if this EdgeEffect's animation is finished. If this method returns false 292 * after a call to {@link #draw(Canvas)} the host widget should schedule another 293 * drawing pass to continue the animation. 294 * 295 * @return true if animation is finished, false if drawing should continue on the next frame. 296 */ isFinished()297 public boolean isFinished() { 298 return mState == STATE_IDLE; 299 } 300 301 /** 302 * Immediately finish the current animation. 303 * After this call {@link #isFinished()} will return true. 304 */ finish()305 public void finish() { 306 mState = STATE_IDLE; 307 mDistance = 0; 308 mVelocity = 0; 309 } 310 311 /** 312 * A view should call this when content is pulled away from an edge by the user. 313 * This will update the state of the current visual effect and its associated animation. 314 * The host view should always {@link android.view.View#invalidate()} after this 315 * and draw the results accordingly. 316 * 317 * <p>Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement 318 * of the pull point is known.</p> 319 * 320 * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to 321 * 1.f (full length of the view) or negative values to express change 322 * back toward the edge reached to initiate the effect. 323 */ onPull(float deltaDistance)324 public void onPull(float deltaDistance) { 325 onPull(deltaDistance, 0.5f); 326 } 327 328 /** 329 * A view should call this when content is pulled away from an edge by the user. 330 * This will update the state of the current visual effect and its associated animation. 331 * The host view should always {@link android.view.View#invalidate()} after this 332 * and draw the results accordingly. 333 * 334 * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to 335 * 1.f (full length of the view) or negative values to express change 336 * back toward the edge reached to initiate the effect. 337 * @param displacement The displacement from the starting side of the effect of the point 338 * initiating the pull. In the case of touch this is the finger position. 339 * Values may be from 0-1. 340 */ onPull(float deltaDistance, float displacement)341 public void onPull(float deltaDistance, float displacement) { 342 int edgeEffectBehavior = getCurrentEdgeEffectBehavior(); 343 if (edgeEffectBehavior == TYPE_NONE) { 344 finish(); 345 return; 346 } 347 final long now = AnimationUtils.currentAnimationTimeMillis(); 348 mTargetDisplacement = displacement; 349 if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration 350 && edgeEffectBehavior == TYPE_GLOW) { 351 return; 352 } 353 if (mState != STATE_PULL) { 354 if (edgeEffectBehavior == TYPE_STRETCH) { 355 // Restore the mPullDistance to the fraction it is currently showing -- we want 356 // to "catch" the current stretch value. 357 mPullDistance = mDistance; 358 } else { 359 mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY); 360 } 361 } 362 mState = STATE_PULL; 363 364 mStartTime = now; 365 mDuration = PULL_TIME; 366 367 mPullDistance += deltaDistance; 368 if (edgeEffectBehavior == TYPE_STRETCH) { 369 // Don't allow stretch beyond 1 370 mPullDistance = Math.min(1f, mPullDistance); 371 } 372 mDistance = Math.max(0f, mPullDistance); 373 mVelocity = 0; 374 375 if (mPullDistance == 0) { 376 mGlowScaleY = mGlowScaleYStart = 0; 377 mGlowAlpha = mGlowAlphaStart = 0; 378 } else { 379 final float absdd = Math.abs(deltaDistance); 380 mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, 381 mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); 382 383 final float scale = (float) (Math.max(0, 1 - 1 / 384 Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3d) / 0.7d); 385 386 mGlowScaleY = mGlowScaleYStart = scale; 387 } 388 389 mGlowAlphaFinish = mGlowAlpha; 390 mGlowScaleYFinish = mGlowScaleY; 391 if (edgeEffectBehavior == TYPE_STRETCH && mDistance == 0) { 392 mState = STATE_IDLE; 393 } 394 } 395 396 /** 397 * A view should call this when content is pulled away from an edge by the user. 398 * This will update the state of the current visual effect and its associated animation. 399 * The host view should always {@link android.view.View#invalidate()} after this 400 * and draw the results accordingly. This works similarly to {@link #onPull(float, float)}, 401 * but returns the amount of <code>deltaDistance</code> that has been consumed. If the 402 * {@link #getDistance()} is currently 0 and <code>deltaDistance</code> is negative, this 403 * function will return 0 and the drawn value will remain unchanged. 404 * 405 * This method can be used to reverse the effect from a pull or absorb and partially consume 406 * some of a motion: 407 * 408 * <pre class="prettyprint"> 409 * if (deltaY < 0) { 410 * float consumed = edgeEffect.onPullDistance(deltaY / getHeight(), x / getWidth()); 411 * deltaY -= consumed * getHeight(); 412 * if (edgeEffect.getDistance() == 0f) edgeEffect.onRelease(); 413 * } 414 * </pre> 415 * 416 * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to 417 * 1.f (full length of the view) or negative values to express change 418 * back toward the edge reached to initiate the effect. 419 * @param displacement The displacement from the starting side of the effect of the point 420 * initiating the pull. In the case of touch this is the finger position. 421 * Values may be from 0-1. 422 * @return The amount of <code>deltaDistance</code> that was consumed, a number between 423 * 0 and <code>deltaDistance</code>. 424 */ onPullDistance(float deltaDistance, float displacement)425 public float onPullDistance(float deltaDistance, float displacement) { 426 int edgeEffectBehavior = getCurrentEdgeEffectBehavior(); 427 if (edgeEffectBehavior == TYPE_NONE) { 428 return 0f; 429 } 430 float finalDistance = Math.max(0f, deltaDistance + mDistance); 431 float delta = finalDistance - mDistance; 432 if (delta == 0f && mDistance == 0f) { 433 return 0f; // No pull, don't do anything. 434 } 435 436 if (mState != STATE_PULL && mState != STATE_PULL_DECAY && edgeEffectBehavior == TYPE_GLOW) { 437 // Catch the edge glow in the middle of an animation. 438 mPullDistance = mDistance; 439 mState = STATE_PULL; 440 } 441 onPull(delta, displacement); 442 return delta; 443 } 444 445 /** 446 * Returns the pull distance needed to be released to remove the showing effect. 447 * It is determined by the {@link #onPull(float, float)} <code>deltaDistance</code> and 448 * any animating values, including from {@link #onAbsorb(int)} and {@link #onRelease()}. 449 * 450 * This can be used in conjunction with {@link #onPullDistance(float, float)} to 451 * release the currently showing effect. 452 * 453 * @return The pull distance that must be released to remove the showing effect. 454 */ getDistance()455 public float getDistance() { 456 return mDistance; 457 } 458 459 /** 460 * Call when the object is released after being pulled. 461 * This will begin the "decay" phase of the effect. After calling this method 462 * the host view should {@link android.view.View#invalidate()} and thereby 463 * draw the results accordingly. 464 */ onRelease()465 public void onRelease() { 466 mPullDistance = 0; 467 468 if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { 469 return; 470 } 471 472 mState = STATE_RECEDE; 473 mGlowAlphaStart = mGlowAlpha; 474 mGlowScaleYStart = mGlowScaleY; 475 476 mGlowAlphaFinish = 0.f; 477 mGlowScaleYFinish = 0.f; 478 mVelocity = 0.f; 479 480 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 481 mDuration = RECEDE_TIME; 482 } 483 484 /** 485 * Call when the effect absorbs an impact at the given velocity. 486 * Used when a fling reaches the scroll boundary. 487 * 488 * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller}, 489 * the method <code>getCurrVelocity</code> will provide a reasonable approximation 490 * to use here.</p> 491 * 492 * @param velocity Velocity at impact in pixels per second. 493 */ onAbsorb(int velocity)494 public void onAbsorb(int velocity) { 495 int edgeEffectBehavior = getCurrentEdgeEffectBehavior(); 496 if (edgeEffectBehavior == TYPE_STRETCH) { 497 mState = STATE_RECEDE; 498 mVelocity = velocity * ON_ABSORB_VELOCITY_ADJUSTMENT; 499 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 500 } else if (edgeEffectBehavior == TYPE_GLOW) { 501 mState = STATE_ABSORB; 502 mVelocity = 0; 503 velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY); 504 505 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 506 mDuration = 0.15f + (velocity * 0.02f); 507 508 // The glow depends more on the velocity, and therefore starts out 509 // nearly invisible. 510 mGlowAlphaStart = GLOW_ALPHA_START; 511 mGlowScaleYStart = Math.max(mGlowScaleY, 0.f); 512 513 // Growth for the size of the glow should be quadratic to properly 514 // respond 515 // to a user's scrolling speed. The faster the scrolling speed, the more 516 // intense the effect should be for both the size and the saturation. 517 mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 518 1.f); 519 // Alpha should change for the glow as well as size. 520 mGlowAlphaFinish = Math.max( 521 mGlowAlphaStart, 522 Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); 523 mTargetDisplacement = 0.5f; 524 } else { 525 finish(); 526 } 527 } 528 529 /** 530 * Set the color of this edge effect in argb. 531 * 532 * @param color Color in argb 533 */ setColor(@olorInt int color)534 public void setColor(@ColorInt int color) { 535 mPaint.setColor(color); 536 } 537 538 /** 539 * Set or clear the blend mode. A blend mode defines how source pixels 540 * (generated by a drawing command) are composited with the destination pixels 541 * (content of the render target). 542 * <p /> 543 * Pass null to clear any previous blend mode. 544 * <p /> 545 * 546 * @see BlendMode 547 * 548 * @param blendmode May be null. The blend mode to be installed in the paint 549 */ setBlendMode(@ullable BlendMode blendmode)550 public void setBlendMode(@Nullable BlendMode blendmode) { 551 mPaint.setBlendMode(blendmode); 552 } 553 554 /** 555 * Return the color of this edge effect in argb. 556 * @return The color of this edge effect in argb 557 */ 558 @ColorInt getColor()559 public int getColor() { 560 return mPaint.getColor(); 561 } 562 563 /** 564 * Returns the blend mode. A blend mode defines how source pixels 565 * (generated by a drawing command) are composited with the destination pixels 566 * (content of the render target). 567 * <p /> 568 * 569 * @return BlendMode 570 */ 571 @Nullable getBlendMode()572 public BlendMode getBlendMode() { 573 return mPaint.getBlendMode(); 574 } 575 576 /** 577 * Draw into the provided canvas. Assumes that the canvas has been rotated 578 * accordingly and the size has been set. The effect will be drawn the full 579 * width of X=0 to X=width, beginning from Y=0 and extending to some factor < 580 * 1.f of height. The effect will only be visible on a 581 * hardware canvas, e.g. {@link RenderNode#beginRecording()}. 582 * 583 * @param canvas Canvas to draw into 584 * @return true if drawing should continue beyond this frame to continue the 585 * animation 586 */ draw(Canvas canvas)587 public boolean draw(Canvas canvas) { 588 int edgeEffectBehavior = getCurrentEdgeEffectBehavior(); 589 if (edgeEffectBehavior == TYPE_GLOW) { 590 update(); 591 final int count = canvas.save(); 592 593 final float centerX = mBounds.centerX(); 594 final float centerY = mBounds.height() - mRadius; 595 596 canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0); 597 598 final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f; 599 float translateX = mBounds.width() * displacement / 2; 600 601 canvas.clipRect(mBounds); 602 canvas.translate(translateX, 0); 603 mPaint.setAlpha((int) (0xff * mGlowAlpha)); 604 canvas.drawCircle(centerX, centerY, mRadius, mPaint); 605 canvas.restoreToCount(count); 606 } else if (edgeEffectBehavior == TYPE_STRETCH && canvas instanceof RecordingCanvas) { 607 if (mState == STATE_RECEDE) { 608 updateSpring(); 609 } 610 if (mDistance != 0f) { 611 RecordingCanvas recordingCanvas = (RecordingCanvas) canvas; 612 if (mTmpMatrix == null) { 613 mTmpMatrix = new Matrix(); 614 mTmpPoints = new float[12]; 615 } 616 //noinspection deprecation 617 recordingCanvas.getMatrix(mTmpMatrix); 618 619 mTmpPoints[0] = 0; 620 mTmpPoints[1] = 0; // top-left 621 mTmpPoints[2] = mWidth; 622 mTmpPoints[3] = 0; // top-right 623 mTmpPoints[4] = mWidth; 624 mTmpPoints[5] = mHeight; // bottom-right 625 mTmpPoints[6] = 0; 626 mTmpPoints[7] = mHeight; // bottom-left 627 mTmpPoints[8] = mWidth * mDisplacement; 628 mTmpPoints[9] = 0; // drag start point 629 mTmpPoints[10] = mWidth * mDisplacement; 630 mTmpPoints[11] = mHeight * mDistance; // drag point 631 mTmpMatrix.mapPoints(mTmpPoints); 632 633 RenderNode renderNode = recordingCanvas.mNode; 634 635 float left = renderNode.getLeft() 636 + min(mTmpPoints[0], mTmpPoints[2], mTmpPoints[4], mTmpPoints[6]); 637 float top = renderNode.getTop() 638 + min(mTmpPoints[1], mTmpPoints[3], mTmpPoints[5], mTmpPoints[7]); 639 float right = renderNode.getLeft() 640 + max(mTmpPoints[0], mTmpPoints[2], mTmpPoints[4], mTmpPoints[6]); 641 float bottom = renderNode.getTop() 642 + max(mTmpPoints[1], mTmpPoints[3], mTmpPoints[5], mTmpPoints[7]); 643 // assume rotations of increments of 90 degrees 644 float x = mTmpPoints[10] - mTmpPoints[8]; 645 float width = right - left; 646 float vecX = dampStretchVector(Math.max(-1f, Math.min(1f, x / width))); 647 648 float y = mTmpPoints[11] - mTmpPoints[9]; 649 float height = bottom - top; 650 float vecY = dampStretchVector(Math.max(-1f, Math.min(1f, y / height))); 651 652 boolean hasValidVectors = Float.isFinite(vecX) && Float.isFinite(vecY); 653 if (right > left && bottom > top && mWidth > 0 && mHeight > 0 && hasValidVectors) { 654 renderNode.stretch( 655 vecX, // horizontal stretch intensity 656 vecY, // vertical stretch intensity 657 mWidth, // max horizontal stretch in pixels 658 mHeight // max vertical stretch in pixels 659 ); 660 } 661 } 662 } else { 663 // Animations have been disabled or this is TYPE_STRETCH and drawing into a Canvas 664 // that isn't a Recording Canvas, so no effect can be shown. Just end the effect. 665 mState = STATE_IDLE; 666 mDistance = 0; 667 mVelocity = 0; 668 } 669 670 boolean oneLastFrame = false; 671 if (mState == STATE_RECEDE && mDistance == 0 && mVelocity == 0) { 672 mState = STATE_IDLE; 673 oneLastFrame = true; 674 } 675 676 return mState != STATE_IDLE || oneLastFrame; 677 } 678 min(float f1, float f2, float f3, float f4)679 private float min(float f1, float f2, float f3, float f4) { 680 float min = Math.min(f1, f2); 681 min = Math.min(min, f3); 682 return Math.min(min, f4); 683 } 684 max(float f1, float f2, float f3, float f4)685 private float max(float f1, float f2, float f3, float f4) { 686 float max = Math.max(f1, f2); 687 max = Math.max(max, f3); 688 return Math.max(max, f4); 689 } 690 691 /** 692 * Return the maximum height that the edge effect will be drawn at given the original 693 * {@link #setSize(int, int) input size}. 694 * @return The maximum height of the edge effect 695 */ getMaxHeight()696 public int getMaxHeight() { 697 return (int) mHeight; 698 } 699 update()700 private void update() { 701 final long time = AnimationUtils.currentAnimationTimeMillis(); 702 final float t = Math.min((time - mStartTime) / mDuration, 1.f); 703 704 final float interp = mInterpolator.getInterpolation(t); 705 706 mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; 707 mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; 708 if (mState != STATE_PULL) { 709 mDistance = calculateDistanceFromGlowValues(mGlowScaleY, mGlowAlpha); 710 } 711 mDisplacement = (mDisplacement + mTargetDisplacement) / 2; 712 713 if (t >= 1.f - EPSILON) { 714 switch (mState) { 715 case STATE_ABSORB: 716 mState = STATE_RECEDE; 717 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 718 mDuration = RECEDE_TIME; 719 720 mGlowAlphaStart = mGlowAlpha; 721 mGlowScaleYStart = mGlowScaleY; 722 723 // After absorb, the glow should fade to nothing. 724 mGlowAlphaFinish = 0.f; 725 mGlowScaleYFinish = 0.f; 726 break; 727 case STATE_PULL: 728 mState = STATE_PULL_DECAY; 729 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 730 mDuration = PULL_DECAY_TIME; 731 732 mGlowAlphaStart = mGlowAlpha; 733 mGlowScaleYStart = mGlowScaleY; 734 735 // After pull, the glow should fade to nothing. 736 mGlowAlphaFinish = 0.f; 737 mGlowScaleYFinish = 0.f; 738 break; 739 case STATE_PULL_DECAY: 740 mState = STATE_RECEDE; 741 break; 742 case STATE_RECEDE: 743 mState = STATE_IDLE; 744 break; 745 } 746 } 747 } 748 updateSpring()749 private void updateSpring() { 750 final long time = AnimationUtils.currentAnimationTimeMillis(); 751 final float deltaT = (time - mStartTime) / 1000f; // Convert from millis to seconds 752 if (deltaT < 0.001f) { 753 return; // Must have at least 1 ms difference 754 } 755 mStartTime = time; 756 757 if (Math.abs(mVelocity) <= LINEAR_VELOCITY_TAKE_OVER 758 && Math.abs(mDistance * mHeight) < LINEAR_DISTANCE_TAKE_OVER 759 && Math.signum(mVelocity) == -Math.signum(mDistance) 760 ) { 761 // This is close. The spring will slowly reach the destination. Instead, we 762 // will interpolate linearly so that it arrives at its destination quicker. 763 mVelocity = Math.signum(mVelocity) * LINEAR_VELOCITY_TAKE_OVER; 764 765 float targetDistance = mDistance + (mVelocity * deltaT / mHeight); 766 if (Math.signum(targetDistance) != Math.signum(mDistance)) { 767 // We have arrived 768 mDistance = 0; 769 mVelocity = 0; 770 } else { 771 mDistance = targetDistance; 772 } 773 return; 774 } 775 final double mDampedFreq = NATURAL_FREQUENCY * Math.sqrt(1 - DAMPING_RATIO * DAMPING_RATIO); 776 777 // We're always underdamped, so we can use only those equations: 778 double cosCoeff = mDistance * mHeight; 779 double sinCoeff = (1 / mDampedFreq) * (DAMPING_RATIO * NATURAL_FREQUENCY 780 * mDistance * mHeight + mVelocity); 781 double distance = Math.pow(Math.E, -DAMPING_RATIO * NATURAL_FREQUENCY * deltaT) 782 * (cosCoeff * Math.cos(mDampedFreq * deltaT) 783 + sinCoeff * Math.sin(mDampedFreq * deltaT)); 784 double velocity = distance * (-NATURAL_FREQUENCY) * DAMPING_RATIO 785 + Math.pow(Math.E, -DAMPING_RATIO * NATURAL_FREQUENCY * deltaT) 786 * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT) 787 + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT)); 788 mDistance = (float) distance / mHeight; 789 mVelocity = (float) velocity; 790 if (mDistance > 1f) { 791 mDistance = 1f; 792 mVelocity = 0f; 793 } 794 if (isAtEquilibrium()) { 795 mDistance = 0; 796 mVelocity = 0; 797 } 798 } 799 800 /** 801 * @return The estimated pull distance as calculated from mGlowScaleY. 802 */ calculateDistanceFromGlowValues(float scale, float alpha)803 private float calculateDistanceFromGlowValues(float scale, float alpha) { 804 if (scale >= 1f) { 805 // It should asymptotically approach 1, but not reach there. 806 // Here, we're just choosing a value that is large. 807 return 1f; 808 } 809 if (scale > 0f) { 810 float v = 1f / 0.7f / (mGlowScaleY - 1f); 811 return v * v / mBounds.height(); 812 } 813 return alpha / PULL_DISTANCE_ALPHA_GLOW_FACTOR; 814 } 815 816 /** 817 * @return true if the spring used for calculating the stretch animation is 818 * considered at rest or false if it is still animating. 819 */ isAtEquilibrium()820 private boolean isAtEquilibrium() { 821 double displacement = mDistance * mHeight; // in pixels 822 double velocity = mVelocity; 823 824 // Don't allow displacement to drop below 0. We don't want it stretching the opposite 825 // direction if it is flung that way. We also want to stop the animation as soon as 826 // it gets very close to its destination. 827 return displacement < 0 || (Math.abs(velocity) < VELOCITY_THRESHOLD 828 && displacement < VALUE_THRESHOLD); 829 } 830 dampStretchVector(float normalizedVec)831 private float dampStretchVector(float normalizedVec) { 832 float sign = normalizedVec > 0 ? 1f : -1f; 833 float overscroll = Math.abs(normalizedVec); 834 float linearIntensity = LINEAR_STRETCH_INTENSITY * overscroll; 835 double scalar = Math.E / SCROLL_DIST_AFFECTED_BY_EXP_STRETCH; 836 double expIntensity = EXP_STRETCH_INTENSITY * (1 - Math.exp(-overscroll * scalar)); 837 return sign * (float) (linearIntensity + expIntensity); 838 } 839 } 840