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