1/* 2 * Copyright (C) 2013 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 17package android.support.v4.widget; 18 19import android.content.res.Resources; 20import android.os.SystemClock; 21import android.support.v4.view.MotionEventCompat; 22import android.support.v4.view.ViewCompat; 23import android.util.DisplayMetrics; 24import android.view.MotionEvent; 25import android.view.View; 26import android.view.ViewConfiguration; 27import android.view.animation.AccelerateInterpolator; 28import android.view.animation.AnimationUtils; 29import android.view.animation.Interpolator; 30 31/** 32 * AutoScrollHelper is a utility class for adding automatic edge-triggered 33 * scrolling to Views. 34 * <p> 35 * <b>Note:</b> Implementing classes are responsible for overriding the 36 * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and 37 * {@link #canTargetScrollVertically} methods. See 38 * {@link ListViewAutoScrollHelper} for a {@link android.widget.ListView} 39 * -specific implementation. 40 * <p> 41 * <h1>Activation</h1> Automatic scrolling starts when the user touches within 42 * an activation area. By default, activation areas are defined as the top, 43 * left, right, and bottom 20% of the host view's total area. Touching within 44 * the top activation area scrolls up, left scrolls to the left, and so on. 45 * <p> 46 * As the user touches closer to the extreme edge of the activation area, 47 * scrolling accelerates up to a maximum velocity. When using the default edge 48 * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds 49 * will scroll at the maximum velocity. 50 * <p> 51 * The following activation properties may be configured: 52 * <ul> 53 * <li>Delay after entering activation area before auto-scrolling begins, see 54 * {@link #setActivationDelay}. Default value is 55 * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps. 56 * <li>Location of activation areas, see {@link #setEdgeType}. Default value is 57 * {@link #EDGE_TYPE_INSIDE_EXTEND}. 58 * <li>Size of activation areas relative to view size, see 59 * {@link #setRelativeEdges}. Default value is 20% for both vertical and 60 * horizontal edges. 61 * <li>Maximum size used to constrain relative size, see 62 * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}. 63 * </ul> 64 * <h1>Scrolling</h1> When automatic scrolling is active, the helper will 65 * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets. 66 * <p> 67 * The following scrolling properties may be configured: 68 * <ul> 69 * <li>Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default 70 * value is 500 milliseconds. 71 * <li>Acceleration ramp-down duration, see {@link #setRampDownDuration}. 72 * Default value is 500 milliseconds. 73 * <li>Target velocity relative to view size, see {@link #setRelativeVelocity}. 74 * Default value is 100% per second for both vertical and horizontal. 75 * <li>Minimum velocity used to constrain relative velocity, see 76 * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the 77 * larger of either this value or the relative target value. Default value is 78 * approximately 5 centimeters or 315 dips per second. 79 * <li>Maximum velocity used to constrain relative velocity, see 80 * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or 81 * 1575 dips per second. 82 * </ul> 83 */ 84public abstract class AutoScrollHelper implements View.OnTouchListener { 85 /** 86 * Constant passed to {@link #setRelativeEdges} or 87 * {@link #setRelativeVelocity}. Using this value ensures that the computed 88 * relative value is ignored and the absolute maximum value is always used. 89 */ 90 public static final float RELATIVE_UNSPECIFIED = 0; 91 92 /** 93 * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity}, 94 * or {@link #setMinimumVelocity}. Using this value ensures that the 95 * computed relative value is always used without constraining to a 96 * particular minimum or maximum value. 97 */ 98 public static final float NO_MAX = Float.MAX_VALUE; 99 100 /** 101 * Constant passed to {@link #setMaximumEdges}, or 102 * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this 103 * value ensures that the computed relative value is always used without 104 * constraining to a particular minimum or maximum value. 105 */ 106 public static final float NO_MIN = 0; 107 108 /** 109 * Edge type that specifies an activation area starting at the view bounds 110 * and extending inward. Moving outside the view bounds will stop scrolling. 111 * 112 * @see #setEdgeType 113 */ 114 public static final int EDGE_TYPE_INSIDE = 0; 115 116 /** 117 * Edge type that specifies an activation area starting at the view bounds 118 * and extending inward. After activation begins, moving outside the view 119 * bounds will continue scrolling. 120 * 121 * @see #setEdgeType 122 */ 123 public static final int EDGE_TYPE_INSIDE_EXTEND = 1; 124 125 /** 126 * Edge type that specifies an activation area starting at the view bounds 127 * and extending outward. Moving inside the view bounds will stop scrolling. 128 * 129 * @see #setEdgeType 130 */ 131 public static final int EDGE_TYPE_OUTSIDE = 2; 132 133 private static final int HORIZONTAL = 0; 134 private static final int VERTICAL = 1; 135 136 /** Scroller used to control acceleration toward maximum velocity. */ 137 private final ClampedScroller mScroller = new ClampedScroller(); 138 139 /** Interpolator used to scale velocity with touch position. */ 140 private final Interpolator mEdgeInterpolator = new AccelerateInterpolator(); 141 142 /** The view to auto-scroll. Might not be the source of touch events. */ 143 private final View mTarget; 144 145 /** Runnable used to animate scrolling. */ 146 private Runnable mRunnable; 147 148 /** Edge insets used to activate auto-scrolling. */ 149 private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED }; 150 151 /** Clamping values for edge insets used to activate auto-scrolling. */ 152 private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX }; 153 154 /** The type of edge being used. */ 155 private int mEdgeType; 156 157 /** Delay after entering an activation edge before auto-scrolling begins. */ 158 private int mActivationDelay; 159 160 /** Relative scrolling velocity at maximum edge distance. */ 161 private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED }; 162 163 /** Clamping values used for scrolling velocity. */ 164 private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN }; 165 166 /** Clamping values used for scrolling velocity. */ 167 private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX }; 168 169 /** Whether to start activation immediately. */ 170 private boolean mAlreadyDelayed; 171 172 /** Whether to reset the scroller start time on the next animation. */ 173 private boolean mNeedsReset; 174 175 /** Whether to send a cancel motion event to the target view. */ 176 private boolean mNeedsCancel; 177 178 /** Whether the auto-scroller is actively scrolling. */ 179 private boolean mAnimating; 180 181 /** Whether the auto-scroller is enabled. */ 182 private boolean mEnabled; 183 184 /** Whether the auto-scroller consumes events when scrolling. */ 185 private boolean mExclusive; 186 187 // Default values. 188 private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND; 189 private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315; 190 private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575; 191 private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX; 192 private static final float DEFAULT_RELATIVE_EDGE = 0.2f; 193 private static final float DEFAULT_RELATIVE_VELOCITY = 1f; 194 private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout(); 195 private static final int DEFAULT_RAMP_UP_DURATION = 500; 196 private static final int DEFAULT_RAMP_DOWN_DURATION = 500; 197 198 /** 199 * Creates a new helper for scrolling the specified target view. 200 * <p> 201 * The resulting helper may be configured by chaining setter calls and 202 * should be set as a touch listener on the target view. 203 * <p> 204 * By default, the helper is disabled and will not respond to touch events 205 * until it is enabled using {@link #setEnabled}. 206 * 207 * @param target The view to automatically scroll. 208 */ 209 public AutoScrollHelper(View target) { 210 mTarget = target; 211 212 final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); 213 final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f); 214 final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f); 215 setMaximumVelocity(maxVelocity, maxVelocity); 216 setMinimumVelocity(minVelocity, minVelocity); 217 218 setEdgeType(DEFAULT_EDGE_TYPE); 219 setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE); 220 setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE); 221 setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY); 222 setActivationDelay(DEFAULT_ACTIVATION_DELAY); 223 setRampUpDuration(DEFAULT_RAMP_UP_DURATION); 224 setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION); 225 } 226 227 /** 228 * Sets whether the scroll helper is enabled and should respond to touch 229 * events. 230 * 231 * @param enabled Whether the scroll helper is enabled. 232 * @return The scroll helper, which may used to chain setter calls. 233 */ 234 public AutoScrollHelper setEnabled(boolean enabled) { 235 if (mEnabled && !enabled) { 236 requestStop(); 237 } 238 239 mEnabled = enabled; 240 return this; 241 } 242 243 /** 244 * @return True if this helper is enabled and responding to touch events. 245 */ 246 public boolean isEnabled() { 247 return mEnabled; 248 } 249 250 /** 251 * Enables or disables exclusive handling of touch events during scrolling. 252 * By default, exclusive handling is disabled and the target view receives 253 * all touch events. 254 * <p> 255 * When enabled, {@link #onTouch} will return true if the helper is 256 * currently scrolling and false otherwise. 257 * 258 * @param exclusive True to exclusively handle touch events during scrolling, 259 * false to allow the target view to receive all touch events. 260 * @return The scroll helper, which may used to chain setter calls. 261 */ 262 public AutoScrollHelper setExclusive(boolean exclusive) { 263 mExclusive = exclusive; 264 return this; 265 } 266 267 /** 268 * Indicates whether the scroll helper handles touch events exclusively 269 * during scrolling. 270 * 271 * @return True if exclusive handling of touch events during scrolling is 272 * enabled, false otherwise. 273 * @see #setExclusive(boolean) 274 */ 275 public boolean isExclusive() { 276 return mExclusive; 277 } 278 279 /** 280 * Sets the absolute maximum scrolling velocity. 281 * <p> 282 * If relative velocity is not specified, scrolling will always reach the 283 * same maximum velocity. If both relative and maximum velocities are 284 * specified, the maximum velocity will be used to clamp the calculated 285 * relative velocity. 286 * 287 * @param horizontalMax The maximum horizontal scrolling velocity, or 288 * {@link #NO_MAX} to leave the relative value unconstrained. 289 * @param verticalMax The maximum vertical scrolling velocity, or 290 * {@link #NO_MAX} to leave the relative value unconstrained. 291 * @return The scroll helper, which may used to chain setter calls. 292 */ 293 public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) { 294 mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f; 295 mMaximumVelocity[VERTICAL] = verticalMax / 1000f; 296 return this; 297 } 298 299 /** 300 * Sets the absolute minimum scrolling velocity. 301 * <p> 302 * If both relative and minimum velocities are specified, the minimum 303 * velocity will be used to clamp the calculated relative velocity. 304 * 305 * @param horizontalMin The minimum horizontal scrolling velocity, or 306 * {@link #NO_MIN} to leave the relative value unconstrained. 307 * @param verticalMin The minimum vertical scrolling velocity, or 308 * {@link #NO_MIN} to leave the relative value unconstrained. 309 * @return The scroll helper, which may used to chain setter calls. 310 */ 311 public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) { 312 mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f; 313 mMinimumVelocity[VERTICAL] = verticalMin / 1000f; 314 return this; 315 } 316 317 /** 318 * Sets the target scrolling velocity relative to the host view's 319 * dimensions. 320 * <p> 321 * If both relative and maximum velocities are specified, the maximum 322 * velocity will be used to clamp the calculated relative velocity. 323 * 324 * @param horizontal The target horizontal velocity as a fraction of the 325 * host view width per second, or {@link #RELATIVE_UNSPECIFIED} 326 * to ignore. 327 * @param vertical The target vertical velocity as a fraction of the host 328 * view height per second, or {@link #RELATIVE_UNSPECIFIED} to 329 * ignore. 330 * @return The scroll helper, which may used to chain setter calls. 331 */ 332 public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) { 333 mRelativeVelocity[HORIZONTAL] = horizontal / 1000f; 334 mRelativeVelocity[VERTICAL] = vertical / 1000f; 335 return this; 336 } 337 338 /** 339 * Sets the activation edge type, one of: 340 * <ul> 341 * <li>{@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside 342 * the bounds of the host view. If touch moves outside the bounds, scrolling 343 * will stop. 344 * <li>{@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to 345 * scroll when touch moves outside the bounds of the host view. 346 * <li>{@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches 347 * that move outside the bounds of the host view. 348 * </ul> 349 * 350 * @param type The type of edge to use. 351 * @return The scroll helper, which may used to chain setter calls. 352 */ 353 public AutoScrollHelper setEdgeType(int type) { 354 mEdgeType = type; 355 return this; 356 } 357 358 /** 359 * Sets the activation edge size relative to the host view's dimensions. 360 * <p> 361 * If both relative and maximum edges are specified, the maximum edge will 362 * be used to constrain the calculated relative edge size. 363 * 364 * @param horizontal The horizontal edge size as a fraction of the host view 365 * width, or {@link #RELATIVE_UNSPECIFIED} to always use the 366 * maximum value. 367 * @param vertical The vertical edge size as a fraction of the host view 368 * height, or {@link #RELATIVE_UNSPECIFIED} to always use the 369 * maximum value. 370 * @return The scroll helper, which may used to chain setter calls. 371 */ 372 public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) { 373 mRelativeEdges[HORIZONTAL] = horizontal; 374 mRelativeEdges[VERTICAL] = vertical; 375 return this; 376 } 377 378 /** 379 * Sets the absolute maximum edge size. 380 * <p> 381 * If relative edge size is not specified, activation edges will always be 382 * the maximum edge size. If both relative and maximum edges are specified, 383 * the maximum edge will be used to constrain the calculated relative edge 384 * size. 385 * 386 * @param horizontalMax The maximum horizontal edge size in pixels, or 387 * {@link #NO_MAX} to use the unconstrained calculated relative 388 * value. 389 * @param verticalMax The maximum vertical edge size in pixels, or 390 * {@link #NO_MAX} to use the unconstrained calculated relative 391 * value. 392 * @return The scroll helper, which may used to chain setter calls. 393 */ 394 public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) { 395 mMaximumEdges[HORIZONTAL] = horizontalMax; 396 mMaximumEdges[VERTICAL] = verticalMax; 397 return this; 398 } 399 400 /** 401 * Sets the delay after entering an activation edge before activation of 402 * auto-scrolling. By default, the activation delay is set to 403 * {@link ViewConfiguration#getTapTimeout()}. 404 * <p> 405 * Specifying a delay of zero will start auto-scrolling immediately after 406 * the touch position enters an activation edge. 407 * 408 * @param delayMillis The activation delay in milliseconds. 409 * @return The scroll helper, which may used to chain setter calls. 410 */ 411 public AutoScrollHelper setActivationDelay(int delayMillis) { 412 mActivationDelay = delayMillis; 413 return this; 414 } 415 416 /** 417 * Sets the amount of time after activation of auto-scrolling that is takes 418 * to reach target velocity for the current touch position. 419 * <p> 420 * Specifying a duration greater than zero prevents sudden jumps in 421 * velocity. 422 * 423 * @param durationMillis The ramp-up duration in milliseconds. 424 * @return The scroll helper, which may used to chain setter calls. 425 */ 426 public AutoScrollHelper setRampUpDuration(int durationMillis) { 427 mScroller.setRampUpDuration(durationMillis); 428 return this; 429 } 430 431 /** 432 * Sets the amount of time after de-activation of auto-scrolling that is 433 * takes to slow to a stop. 434 * <p> 435 * Specifying a duration greater than zero prevents sudden jumps in 436 * velocity. 437 * 438 * @param durationMillis The ramp-down duration in milliseconds. 439 * @return The scroll helper, which may used to chain setter calls. 440 */ 441 public AutoScrollHelper setRampDownDuration(int durationMillis) { 442 mScroller.setRampDownDuration(durationMillis); 443 return this; 444 } 445 446 /** 447 * Handles touch events by activating automatic scrolling, adjusting scroll 448 * velocity, or stopping. 449 * <p> 450 * If {@link #isExclusive()} is false, always returns false so that 451 * the host view may handle touch events. Otherwise, returns true when 452 * automatic scrolling is active and false otherwise. 453 */ 454 @Override 455 public boolean onTouch(View v, MotionEvent event) { 456 if (!mEnabled) { 457 return false; 458 } 459 460 final int action = MotionEventCompat.getActionMasked(event); 461 switch (action) { 462 case MotionEvent.ACTION_DOWN: 463 mNeedsCancel = true; 464 mAlreadyDelayed = false; 465 // $FALL-THROUGH$ 466 case MotionEvent.ACTION_MOVE: 467 final float xTargetVelocity = computeTargetVelocity( 468 HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth()); 469 final float yTargetVelocity = computeTargetVelocity( 470 VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight()); 471 mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity); 472 473 // If the auto scroller was not previously active, but it should 474 // be, then update the state and start animations. 475 if (!mAnimating && shouldAnimate()) { 476 startAnimating(); 477 } 478 break; 479 case MotionEvent.ACTION_UP: 480 case MotionEvent.ACTION_CANCEL: 481 requestStop(); 482 break; 483 } 484 485 return mExclusive && mAnimating; 486 } 487 488 /** 489 * @return whether the target is able to scroll in the requested direction 490 */ 491 private boolean shouldAnimate() { 492 final ClampedScroller scroller = mScroller; 493 final int verticalDirection = scroller.getVerticalDirection(); 494 final int horizontalDirection = scroller.getHorizontalDirection(); 495 496 return verticalDirection != 0 && canTargetScrollVertically(verticalDirection) 497 || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection); 498 } 499 500 /** 501 * Starts the scroll animation. 502 */ 503 private void startAnimating() { 504 if (mRunnable == null) { 505 mRunnable = new ScrollAnimationRunnable(); 506 } 507 508 mAnimating = true; 509 mNeedsReset = true; 510 511 if (!mAlreadyDelayed && mActivationDelay > 0) { 512 ViewCompat.postOnAnimationDelayed(mTarget, mRunnable, mActivationDelay); 513 } else { 514 mRunnable.run(); 515 } 516 517 // If we start animating again before the user lifts their finger, we 518 // already know it's not a tap and don't need an activation delay. 519 mAlreadyDelayed = true; 520 } 521 522 /** 523 * Requests that the scroll animation slow to a stop. If there is an 524 * activation delay, this may occur between posting the animation and 525 * actually running it. 526 */ 527 private void requestStop() { 528 if (mNeedsReset) { 529 // The animation has been posted, but hasn't run yet. Manually 530 // stopping animation will prevent it from running. 531 mAnimating = false; 532 } else { 533 mScroller.requestStop(); 534 } 535 } 536 537 private float computeTargetVelocity( 538 int direction, float coordinate, float srcSize, float dstSize) { 539 final float relativeEdge = mRelativeEdges[direction]; 540 final float maximumEdge = mMaximumEdges[direction]; 541 final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate); 542 if (value == 0) { 543 // The edge in this direction is not activated. 544 return 0; 545 } 546 547 final float relativeVelocity = mRelativeVelocity[direction]; 548 final float minimumVelocity = mMinimumVelocity[direction]; 549 final float maximumVelocity = mMaximumVelocity[direction]; 550 final float targetVelocity = relativeVelocity * dstSize; 551 552 // Target velocity is adjusted for interpolated edge position, then 553 // clamped to the minimum and maximum values. Later, this value will be 554 // adjusted for time-based acceleration. 555 if (value > 0) { 556 return constrain(value * targetVelocity, minimumVelocity, maximumVelocity); 557 } else { 558 return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity); 559 } 560 } 561 562 /** 563 * Override this method to scroll the target view by the specified number of 564 * pixels. 565 * 566 * @param deltaX The number of pixels to scroll by horizontally. 567 * @param deltaY The number of pixels to scroll by vertically. 568 */ 569 public abstract void scrollTargetBy(int deltaX, int deltaY); 570 571 /** 572 * Override this method to return whether the target view can be scrolled 573 * horizontally in a certain direction. 574 * 575 * @param direction Negative to check scrolling left, positive to check 576 * scrolling right. 577 * @return true if the target view is able to horizontally scroll in the 578 * specified direction. 579 */ 580 public abstract boolean canTargetScrollHorizontally(int direction); 581 582 /** 583 * Override this method to return whether the target view can be scrolled 584 * vertically in a certain direction. 585 * 586 * @param direction Negative to check scrolling up, positive to check 587 * scrolling down. 588 * @return true if the target view is able to vertically scroll in the 589 * specified direction. 590 */ 591 public abstract boolean canTargetScrollVertically(int direction); 592 593 /** 594 * Returns the interpolated position of a touch point relative to an edge 595 * defined by its relative inset, its maximum absolute inset, and the edge 596 * interpolator. 597 * 598 * @param relativeValue The size of the inset relative to the total size. 599 * @param size Total size. 600 * @param maxValue The maximum size of the inset, used to clamp (relative * 601 * total). 602 * @param current Touch position within within the total size. 603 * @return Interpolated value of the touch position within the edge. 604 */ 605 private float getEdgeValue(float relativeValue, float size, float maxValue, float current) { 606 // For now, leading and trailing edges are always the same size. 607 final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue); 608 final float valueLeading = constrainEdgeValue(current, edgeSize); 609 final float valueTrailing = constrainEdgeValue(size - current, edgeSize); 610 final float value = (valueTrailing - valueLeading); 611 final float interpolated; 612 if (value < 0) { 613 interpolated = -mEdgeInterpolator.getInterpolation(-value); 614 } else if (value > 0) { 615 interpolated = mEdgeInterpolator.getInterpolation(value); 616 } else { 617 return 0; 618 } 619 620 return constrain(interpolated, -1, 1); 621 } 622 623 private float constrainEdgeValue(float current, float leading) { 624 if (leading == 0) { 625 return 0; 626 } 627 628 switch (mEdgeType) { 629 case EDGE_TYPE_INSIDE: 630 case EDGE_TYPE_INSIDE_EXTEND: 631 if (current < leading) { 632 if (current >= 0) { 633 // Movement up to the edge is scaled. 634 return 1f - current / leading; 635 } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) { 636 // Movement beyond the edge is always maximum. 637 return 1f; 638 } 639 } 640 break; 641 case EDGE_TYPE_OUTSIDE: 642 if (current < 0) { 643 // Movement beyond the edge is scaled. 644 return current / -leading; 645 } 646 break; 647 } 648 649 return 0; 650 } 651 652 private static int constrain(int value, int min, int max) { 653 if (value > max) { 654 return max; 655 } else if (value < min) { 656 return min; 657 } else { 658 return value; 659 } 660 } 661 662 private static float constrain(float value, float min, float max) { 663 if (value > max) { 664 return max; 665 } else if (value < min) { 666 return min; 667 } else { 668 return value; 669 } 670 } 671 672 /** 673 * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view, 674 * canceling any ongoing touch events. 675 */ 676 private void cancelTargetTouch() { 677 final long eventTime = SystemClock.uptimeMillis(); 678 final MotionEvent cancel = MotionEvent.obtain( 679 eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0); 680 mTarget.onTouchEvent(cancel); 681 cancel.recycle(); 682 } 683 684 private class ScrollAnimationRunnable implements Runnable { 685 @Override 686 public void run() { 687 if (!mAnimating) { 688 return; 689 } 690 691 if (mNeedsReset) { 692 mNeedsReset = false; 693 mScroller.start(); 694 } 695 696 final ClampedScroller scroller = mScroller; 697 if (scroller.isFinished() || !shouldAnimate()) { 698 mAnimating = false; 699 return; 700 } 701 702 if (mNeedsCancel) { 703 mNeedsCancel = false; 704 cancelTargetTouch(); 705 } 706 707 scroller.computeScrollDelta(); 708 709 final int deltaX = scroller.getDeltaX(); 710 final int deltaY = scroller.getDeltaY(); 711 scrollTargetBy(deltaX, deltaY); 712 713 // Keep going until the scroller has permanently stopped. 714 ViewCompat.postOnAnimation(mTarget, this); 715 } 716 } 717 718 /** 719 * Scroller whose velocity follows the curve of an {@link Interpolator} and 720 * is clamped to the interpolated 0f value before starting and the 721 * interpolated 1f value after a specified duration. 722 */ 723 private static class ClampedScroller { 724 private int mRampUpDuration; 725 private int mRampDownDuration; 726 private float mTargetVelocityX; 727 private float mTargetVelocityY; 728 729 private long mStartTime; 730 731 private long mDeltaTime; 732 private int mDeltaX; 733 private int mDeltaY; 734 735 private long mStopTime; 736 private float mStopValue; 737 private int mEffectiveRampDown; 738 739 /** 740 * Creates a new ramp-up scroller that reaches full velocity after a 741 * specified duration. 742 */ 743 public ClampedScroller() { 744 mStartTime = Long.MIN_VALUE; 745 mStopTime = -1; 746 mDeltaTime = 0; 747 mDeltaX = 0; 748 mDeltaY = 0; 749 } 750 751 public void setRampUpDuration(int durationMillis) { 752 mRampUpDuration = durationMillis; 753 } 754 755 public void setRampDownDuration(int durationMillis) { 756 mRampDownDuration = durationMillis; 757 } 758 759 /** 760 * Starts the scroller at the current animation time. 761 */ 762 public void start() { 763 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 764 mStopTime = -1; 765 mDeltaTime = mStartTime; 766 mStopValue = 0.5f; 767 mDeltaX = 0; 768 mDeltaY = 0; 769 } 770 771 /** 772 * Stops the scroller at the current animation time. 773 */ 774 public void requestStop() { 775 final long currentTime = AnimationUtils.currentAnimationTimeMillis(); 776 mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration); 777 mStopValue = getValueAt(currentTime); 778 mStopTime = currentTime; 779 } 780 781 public boolean isFinished() { 782 return mStopTime > 0 783 && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown; 784 } 785 786 private float getValueAt(long currentTime) { 787 if (currentTime < mStartTime) { 788 return 0f; 789 } else if (mStopTime < 0 || currentTime < mStopTime) { 790 final long elapsedSinceStart = currentTime - mStartTime; 791 return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1); 792 } else { 793 final long elapsedSinceEnd = currentTime - mStopTime; 794 return (1 - mStopValue) + mStopValue 795 * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1); 796 } 797 } 798 799 /** 800 * Interpolates the value along a parabolic curve corresponding to the equation 801 * <code>y = -4x * (x-1)</code>. 802 * 803 * @param value The value to interpolate, between 0 and 1. 804 * @return the interpolated value, between 0 and 1. 805 */ 806 private float interpolateValue(float value) { 807 return -4 * value * value + 4 * value; 808 } 809 810 /** 811 * Computes the current scroll deltas. This usually only be called after 812 * starting the scroller with {@link #start()}. 813 * 814 * @see #getDeltaX() 815 * @see #getDeltaY() 816 */ 817 public void computeScrollDelta() { 818 if (mDeltaTime == 0) { 819 throw new RuntimeException("Cannot compute scroll delta before calling start()"); 820 } 821 822 final long currentTime = AnimationUtils.currentAnimationTimeMillis(); 823 final float value = getValueAt(currentTime); 824 final float scale = interpolateValue(value); 825 final long elapsedSinceDelta = currentTime - mDeltaTime; 826 827 mDeltaTime = currentTime; 828 mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX); 829 mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY); 830 } 831 832 /** 833 * Sets the target velocity for this scroller. 834 * 835 * @param x The target X velocity in pixels per millisecond. 836 * @param y The target Y velocity in pixels per millisecond. 837 */ 838 public void setTargetVelocity(float x, float y) { 839 mTargetVelocityX = x; 840 mTargetVelocityY = y; 841 } 842 843 public int getHorizontalDirection() { 844 return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX)); 845 } 846 847 public int getVerticalDirection() { 848 return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY)); 849 } 850 851 /** 852 * The distance traveled in the X-coordinate computed by the last call 853 * to {@link #computeScrollDelta()}. 854 */ 855 public int getDeltaX() { 856 return mDeltaX; 857 } 858 859 /** 860 * The distance traveled in the Y-coordinate computed by the last call 861 * to {@link #computeScrollDelta()}. 862 */ 863 public int getDeltaY() { 864 return mDeltaY; 865 } 866 } 867} 868