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