├── README.md
└── ReboundScrollView.java
/README.md:
--------------------------------------------------------------------------------
1 | # ReboundScrollView
2 | 完美的阻尼效果类 弹性效果类 可以嵌套任意View 自带滚动的View也可以 跟左右滑动0冲突
3 |
4 | 使用方法:
5 |
45 | * that watches for focus changes initiated outside this ScrollView knows 46 | *
47 | * that it does not have to do anything. 48 | */ 49 | private boolean mScrollViewMovedFocus; 50 | /** 51 | * Position of the last motion event. 52 | */ 53 | 54 | private float mLastMotionY; 55 | /** 56 | * True when the layout has changed but the traversal has not come through 57 | *
58 | * yet. Ideally the view hierarchy would keep track of this for us. 59 | */ 60 | private boolean mIsLayoutDirty = true; 61 | /** 62 | * The child to give focus to in the event that a child has requested focus 63 | * while the layout is dirty. This prevents the scroll from being wrong if 64 | * the child has not been laid out before requesting focus. 65 | */ 66 | 67 | private View mChildToScrollTo = null; 68 | /** 69 | * True if the user is currently dragging this ScrollView around. This is 70 | *
71 | * not the same as 'is being flinged', which can be checked by 72 | *
73 | * mScroller.isFinished() (flinging begins when the user lifts his finger). 74 | */ 75 | 76 | private boolean mIsBeingDragged = false; 77 | /** 78 | * Determines speed during touch scrolling 79 | */ 80 | 81 | private VelocityTracker mVelocityTracker; 82 | /** 83 | * When set to true, the scroll view measure its child to make it fill the 84 | *
85 | * currently visible area. 86 | */ 87 | private boolean mFillViewport; 88 | /** 89 | * Whether arrow scrolling is animated. 90 | */ 91 | 92 | private boolean mSmoothScrollingEnabled = true; 93 | private int mTouchSlop; 94 | private int mMinimumVelocity; 95 | private int mMaximumVelocity; 96 | 97 | /** 98 | * ID of the active pointer. This is used to retain consistency during 99 | *
100 | * drags/flings if multiple pointers are used. 101 | */ 102 | 103 | private int mActivePointerId = INVALID_POINTER; 104 | /** 105 | * Sentinel value for no current active pointer. Used by 106 | *
107 | * {@link #mActivePointerId}. 108 | */ 109 | 110 | private static final int INVALID_POINTER = -1; 111 | public ReboundScrollView(Context context) 112 | { 113 | this(context, null); 114 | } 115 | 116 | 117 | public ReboundScrollView(Context context, AttributeSet attrs) 118 | { 119 | this(context, attrs, 0); 120 | } 121 | public ReboundScrollView(Context context, AttributeSet attrs, int defStyle) 122 | { 123 | super(context, attrs, defStyle); 124 | mContext = context; 125 | initScrollView(); 126 | setFillViewport(true); 127 | initBounce(); 128 | } 129 | 130 | 131 | private void initBounce() 132 | { 133 | metrics = this.mContext.getResources().getDisplayMetrics(); 134 | // init the bouncy scroller, and make sure the layout is being drawn 135 | // after the top padding 136 | mScroller = new Scroller(getContext(), new OvershootInterpolator(OVERSHOOT_TENSION)); 137 | overScrollerSpringbackTask = new Runnable() 138 | { 139 | @Override 140 | public void run() 141 | { 142 | // scroll till after the padding 143 | mScroller.computeScrollOffset(); 144 | scrollTo(0, mScroller.getCurrY()); 145 | if (!mScroller.isFinished()) 146 | { 147 | post(this); 148 | } 149 | } 150 | }; 151 | prevScrollY = getPaddingTop(); 152 | try 153 | { 154 | mScrollXField = View.class.getDeclaredField("mScrollX"); 155 | mScrollYField = View.class.getDeclaredField("mScrollY"); 156 | 157 | } catch (Exception e) 158 | { 159 | hasFailedObtainingScrollFields = true; 160 | } 161 | } 162 | 163 | 164 | private void SetScrollY(int value) 165 | { 166 | if (mScrollYField != null) 167 | { 168 | try 169 | 170 | { 171 | mScrollYField.setInt(this, value); 172 | } catch (Exception e) 173 | { 174 | } 175 | } 176 | } 177 | 178 | 179 | private void SetScrollX(int value) 180 | { 181 | if (mScrollXField != null) 182 | { 183 | try 184 | { 185 | mScrollXField.setInt(this, value); 186 | } catch (Exception e) 187 | { 188 | } 189 | } 190 | } 191 | 192 | 193 | public void initChildPointer() 194 | { 195 | child = getChildAt(0); 196 | child.setPadding(0, 1500, 0, 1500); 197 | } 198 | 199 | 200 | @Override 201 | protected float getTopFadingEdgeStrength() 202 | { 203 | if (getChildCount() == 0) 204 | { 205 | return 0.0f; 206 | } 207 | 208 | 209 | final int length = getVerticalFadingEdgeLength(); 210 | if (getScrollY() < length) 211 | { 212 | return getScrollY() / (float) length; 213 | } 214 | return 1.0f; 215 | } 216 | 217 | 218 | @Override 219 | protected float getBottomFadingEdgeStrength() 220 | { 221 | if (getChildCount() == 0) 222 | { 223 | return 0.0f; 224 | } 225 | 226 | final int length = getVerticalFadingEdgeLength(); 227 | final int bottomEdge = getHeight() - getPaddingBottom(); 228 | final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; 229 | if (span < length) 230 | { 231 | return span / (float) length; 232 | } 233 | return 1.0f; 234 | 235 | } 236 | 237 | 238 | /** 239 | * @return The maximum amount this scroll view will scroll in response to an 240 | *
241 | * arrow event. 242 | */ 243 | 244 | public int getMaxScrollAmount() 245 | { 246 | return (int) (MAX_SCROLL_FACTOR * (getBottom() - getTop())); 247 | } 248 | 249 | 250 | private void initScrollView() 251 | { 252 | mScroller = new Scroller(getContext()); 253 | setFocusable(true); 254 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 255 | setWillNotDraw(false); 256 | final ViewConfiguration configuration = ViewConfiguration.get(mContext); 257 | mTouchSlop = configuration.getScaledTouchSlop(); 258 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 259 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 260 | 261 | setOnTouchListener(this); 262 | 263 | post(new Runnable() 264 | { 265 | public void run() 266 | { 267 | scrollTo(0, child.getPaddingTop()); 268 | } 269 | }); 270 | } 271 | 272 | 273 | @Override 274 | public void addView(View child) 275 | { 276 | if (getChildCount() > 0) 277 | { 278 | throw new IllegalStateException("ScrollView can host only one direct child"); 279 | } 280 | super.addView(child); 281 | initChildPointer(); 282 | } 283 | 284 | 285 | @Override 286 | 287 | public void addView(View child, int index) 288 | 289 | { 290 | if (getChildCount() > 0) 291 | { 292 | throw new IllegalStateException("ScrollView can host only one direct child"); 293 | } 294 | super.addView(child, index); 295 | initChildPointer(); 296 | } 297 | 298 | 299 | @Override 300 | 301 | public void addView(View child, ViewGroup.LayoutParams params) 302 | { 303 | if (getChildCount() > 0) 304 | { 305 | throw new IllegalStateException("ScrollView can host only one direct child"); 306 | } 307 | super.addView(child, params); 308 | initChildPointer(); 309 | } 310 | 311 | @Override 312 | public void addView(View child, int index, ViewGroup.LayoutParams params) 313 | { 314 | if (getChildCount() > 0) 315 | { 316 | throw new IllegalStateException("ScrollView can host only one direct child"); 317 | } 318 | super.addView(child, index, params); 319 | } 320 | 321 | /** 322 | * @return Returns true this ScrollView can be scrolled 323 | */ 324 | private boolean canScroll() 325 | { 326 | View child = getChildAt(0); 327 | if (child != null) 328 | { 329 | int childHeight = child.getHeight(); 330 | return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); 331 | } 332 | return false; 333 | 334 | } 335 | 336 | 337 | /** 338 | * Indicates whether this ScrollView's content is stretched to fill the 339 | *
340 | * viewport. 341 | * 342 | * @return True if the content fills the viewport, false otherwise. 343 | */ 344 | 345 | public boolean isFillViewport() 346 | 347 | { 348 | 349 | return mFillViewport; 350 | 351 | } 352 | 353 | 354 | /** 355 | * Indicates this ScrollView whether it should stretch its content height to 356 | *
357 | * fill the viewport or not. 358 | * 359 | * @param fillViewport True to stretch the content's height to the viewport's 360 | *
361 | * boundaries, false otherwise. 362 | */ 363 | 364 | public void setFillViewport(boolean fillViewport) 365 | 366 | { 367 | 368 | if (fillViewport != mFillViewport) 369 | 370 | { 371 | 372 | mFillViewport = fillViewport; 373 | 374 | requestLayout(); 375 | 376 | } 377 | 378 | } 379 | 380 | 381 | /** 382 | * @return Whether arrow scrolling will animate its transition. 383 | */ 384 | 385 | public boolean isSmoothScrollingEnabled() 386 | 387 | { 388 | 389 | return mSmoothScrollingEnabled; 390 | 391 | } 392 | 393 | 394 | /** 395 | * Set whether arrow scrolling will animate its transition. 396 | * 397 | * @param smoothScrollingEnabled whether arrow scrolling will animate its transition 398 | */ 399 | 400 | public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) 401 | 402 | { 403 | 404 | mSmoothScrollingEnabled = smoothScrollingEnabled; 405 | 406 | } 407 | 408 | 409 | @Override 410 | 411 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 412 | 413 | { 414 | 415 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 416 | 417 | 418 | if (!mFillViewport) 419 | 420 | { 421 | 422 | return; 423 | 424 | } 425 | 426 | 427 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 428 | 429 | if (heightMode == MeasureSpec.UNSPECIFIED) 430 | 431 | { 432 | 433 | return; 434 | 435 | } 436 | 437 | 438 | if (getChildCount() > 0) 439 | 440 | { 441 | 442 | final View child = getChildAt(0); 443 | 444 | int height = getMeasuredHeight(); 445 | 446 | if (child.getMeasuredHeight() < height) 447 | 448 | { 449 | 450 | final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 451 | 452 | 453 | int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width); 454 | 455 | height -= getPaddingTop(); 456 | 457 | height -= getPaddingBottom(); 458 | 459 | int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 460 | 461 | 462 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 463 | 464 | } 465 | 466 | } 467 | 468 | } 469 | 470 | 471 | @Override 472 | 473 | public boolean dispatchKeyEvent(KeyEvent event) 474 | 475 | { 476 | 477 | // Let the focused view and/or our descendants get the key first 478 | 479 | return super.dispatchKeyEvent(event) || executeKeyEvent(event); 480 | 481 | } 482 | 483 | 484 | /** 485 | * You can call this function yourself to have the scroll view perform 486 | *
487 | * scrolling from a key event, just as if the event had been dispatched to 488 | *
489 | * it by the view hierarchy. 490 | * 491 | * @param event The key event to execute. 492 | * @return Return true if the event was handled, else false. 493 | */ 494 | 495 | public boolean executeKeyEvent(KeyEvent event) 496 | 497 | { 498 | 499 | mTempRect.setEmpty(); 500 | 501 | 502 | if (!canScroll()) 503 | 504 | { 505 | 506 | if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) 507 | 508 | { 509 | 510 | View currentFocused = findFocus(); 511 | 512 | if (currentFocused == this) 513 | 514 | currentFocused = null; 515 | 516 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN); 517 | 518 | return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN); 519 | 520 | } 521 | 522 | return false; 523 | 524 | } 525 | 526 | 527 | boolean handled = false; 528 | 529 | if (event.getAction() == KeyEvent.ACTION_DOWN) 530 | 531 | { 532 | 533 | switch (event.getKeyCode()) 534 | 535 | { 536 | 537 | case KeyEvent.KEYCODE_DPAD_UP: 538 | 539 | if (!event.isAltPressed()) 540 | 541 | { 542 | 543 | handled = arrowScroll(View.FOCUS_UP); 544 | 545 | } else 546 | 547 | { 548 | 549 | handled = fullScroll(View.FOCUS_UP); 550 | 551 | } 552 | 553 | break; 554 | 555 | case KeyEvent.KEYCODE_DPAD_DOWN: 556 | 557 | if (!event.isAltPressed()) 558 | 559 | { 560 | 561 | handled = arrowScroll(View.FOCUS_DOWN); 562 | 563 | } else 564 | 565 | { 566 | 567 | handled = fullScroll(View.FOCUS_DOWN); 568 | 569 | } 570 | 571 | break; 572 | 573 | case KeyEvent.KEYCODE_SPACE: 574 | 575 | pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); 576 | 577 | break; 578 | 579 | } 580 | 581 | } 582 | 583 | 584 | return handled; 585 | 586 | } 587 | 588 | 589 | public boolean inChild(int x, int y) 590 | 591 | { 592 | 593 | if (getChildCount() > 0) 594 | 595 | { 596 | 597 | final int scrollY = getScrollY(); 598 | 599 | final View child = getChildAt(0); 600 | 601 | return !(y < child.getTop() - scrollY || y >= child.getBottom() - scrollY || x < child.getLeft() || x >= child.getRight()); 602 | 603 | } 604 | 605 | return false; 606 | 607 | } 608 | 609 | 610 | @Override 611 | 612 | public boolean onInterceptTouchEvent(MotionEvent ev) 613 | 614 | { 615 | 616 | /* 617 | 618 | * This method JUST determines whether we want to intercept the motion. 619 | 620 | * If we return true, onMotionEvent will be called and we do the actual 621 | 622 | * scrolling there. 623 | 624 | */ 625 | 626 | 627 | 628 | /* 629 | 630 | * Shortcut the most recurring case: the user is in the dragging state 631 | 632 | * and he is moving his finger. We want to intercept this motion. 633 | 634 | */ 635 | 636 | final int action = ev.getAction(); 637 | 638 | if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) 639 | 640 | { 641 | 642 | return true; 643 | 644 | } 645 | 646 | 647 | switch (action & MotionEvent.ACTION_MASK) 648 | 649 | { 650 | 651 | case MotionEvent.ACTION_MOVE: 652 | 653 | { 654 | 655 | /* 656 | 657 | * mIsBeingDragged == false, otherwise the shortcut would have 658 | 659 | * caught it. Check whether the user has moved far enough from his 660 | 661 | * original down touch. 662 | 663 | */ 664 | 665 | 666 | 667 | /* 668 | 669 | * Locally do absolute value. mLastMotionY is set to the y value of 670 | 671 | * the down event. 672 | 673 | */ 674 | 675 | final int activePointerId = mActivePointerId; 676 | 677 | if (activePointerId == INVALID_POINTER) 678 | 679 | { 680 | 681 | // If we don't have a valid id, the touch down wasn't on 682 | 683 | // content. 684 | 685 | break; 686 | 687 | } 688 | 689 | 690 | final int pointerIndex = ev.findPointerIndex(activePointerId); 691 | 692 | final float y = ev.getY(pointerIndex); 693 | 694 | final int yDiff = (int) Math.abs(y - mLastMotionY); 695 | 696 | if (yDiff > mTouchSlop) 697 | 698 | { 699 | 700 | mIsBeingDragged = true; 701 | 702 | mLastMotionY = y; 703 | 704 | } 705 | 706 | break; 707 | 708 | } 709 | 710 | 711 | case MotionEvent.ACTION_DOWN: 712 | 713 | { 714 | 715 | final float y = ev.getY(); 716 | 717 | if (!inChild((int) ev.getX(), (int) y)) 718 | 719 | { 720 | 721 | mIsBeingDragged = false; 722 | 723 | break; 724 | 725 | } 726 | 727 | 728 | 729 | /* 730 | 731 | * Remember location of down touch. ACTION_DOWN always refers to 732 | 733 | * pointer index 0. 734 | 735 | */ 736 | 737 | mLastMotionY = y; 738 | 739 | mActivePointerId = ev.getPointerId(0); 740 | 741 | 742 | 743 | /* 744 | 745 | * If being flinged and user touches the screen, initiate drag; 746 | 747 | * otherwise don't. mScroller.isFinished should be false when being 748 | 749 | * flinged. 750 | 751 | */ 752 | 753 | mIsBeingDragged = !mScroller.isFinished(); 754 | 755 | break; 756 | 757 | } 758 | 759 | 760 | case MotionEvent.ACTION_CANCEL: 761 | 762 | case MotionEvent.ACTION_UP: 763 | 764 | /* Release the drag */ 765 | 766 | mIsBeingDragged = false; 767 | 768 | mActivePointerId = INVALID_POINTER; 769 | 770 | break; 771 | 772 | case MotionEvent.ACTION_POINTER_UP: 773 | 774 | onSecondaryPointerUp(ev); 775 | 776 | break; 777 | 778 | } 779 | 780 | 781 | 782 | /* 783 | 784 | * The only time we want to intercept motion events is if we are in the 785 | 786 | * drag mode. 787 | 788 | */ 789 | 790 | return mIsBeingDragged; 791 | 792 | } 793 | 794 | 795 | @Override 796 | 797 | public boolean onTouchEvent(MotionEvent ev) 798 | 799 | { 800 | 801 | 802 | if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) 803 | 804 | { 805 | 806 | // Don't handle edge touches immediately -- they may actually belong 807 | 808 | // to one of our 809 | 810 | // descendants. 811 | 812 | return false; 813 | 814 | } 815 | 816 | 817 | if (mVelocityTracker == null) 818 | 819 | { 820 | 821 | mVelocityTracker = VelocityTracker.obtain(); 822 | 823 | } 824 | 825 | mVelocityTracker.addMovement(ev); 826 | 827 | 828 | final int action = ev.getAction(); 829 | 830 | 831 | switch (action & MotionEvent.ACTION_MASK) 832 | 833 | { 834 | 835 | case MotionEvent.ACTION_DOWN: 836 | 837 | { 838 | 839 | final float y = ev.getY(); 840 | 841 | if (!(mIsBeingDragged = inChild((int) ev.getX(), (int) y))) 842 | 843 | { 844 | 845 | return false; 846 | 847 | } 848 | 849 | 850 | 851 | /* 852 | 853 | * If being flinged and user touches, stop the fling. isFinished 854 | 855 | * will be false if being flinged. 856 | 857 | */ 858 | 859 | if (!mScroller.isFinished()) 860 | 861 | { 862 | 863 | mScroller.abortAnimation(); 864 | 865 | } 866 | 867 | 868 | // Remember where the motion event started 869 | 870 | mLastMotionY = y; 871 | 872 | mActivePointerId = ev.getPointerId(0); 873 | 874 | break; 875 | 876 | } 877 | 878 | case MotionEvent.ACTION_MOVE: 879 | 880 | if (mIsBeingDragged) 881 | 882 | { 883 | 884 | // Scroll to follow the motion event 885 | 886 | final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 887 | 888 | final float y = ev.getY(activePointerIndex); 889 | 890 | final int deltaY = (int) (mLastMotionY - y); 891 | 892 | mLastMotionY = y; 893 | 894 | 895 | if (isOverScrolled()) 896 | 897 | { 898 | 899 | // when overscrolling, move the scroller just half of the 900 | 901 | // finger movement, to make it feel like a spring... 902 | 903 | scrollBy(0, deltaY / 2); 904 | 905 | } else 906 | 907 | { 908 | 909 | scrollBy(0, deltaY); 910 | 911 | } 912 | 913 | } 914 | 915 | break; 916 | 917 | case MotionEvent.ACTION_UP: 918 | 919 | if (mIsBeingDragged) 920 | 921 | { 922 | 923 | final VelocityTracker velocityTracker = mVelocityTracker; 924 | 925 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 926 | 927 | int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 928 | 929 | 930 | if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) 931 | 932 | { 933 | 934 | fling(-initialVelocity); 935 | 936 | } 937 | 938 | 939 | mActivePointerId = INVALID_POINTER; 940 | 941 | mIsBeingDragged = false; 942 | 943 | 944 | if (mVelocityTracker != null) 945 | 946 | { 947 | 948 | mVelocityTracker.recycle(); 949 | 950 | mVelocityTracker = null; 951 | 952 | } 953 | 954 | } 955 | 956 | break; 957 | 958 | case MotionEvent.ACTION_CANCEL: 959 | 960 | if (mIsBeingDragged && getChildCount() > 0) 961 | 962 | { 963 | 964 | mActivePointerId = INVALID_POINTER; 965 | 966 | mIsBeingDragged = false; 967 | 968 | if (mVelocityTracker != null) 969 | 970 | { 971 | 972 | mVelocityTracker.recycle(); 973 | 974 | mVelocityTracker = null; 975 | 976 | } 977 | 978 | } 979 | 980 | break; 981 | 982 | case MotionEvent.ACTION_POINTER_UP: 983 | 984 | onSecondaryPointerUp(ev); 985 | 986 | break; 987 | 988 | } 989 | 990 | return true; 991 | 992 | } 993 | 994 | 995 | public boolean isOverScrolled() 996 | 997 | { 998 | 999 | return (getScrollY() < child.getPaddingTop() || getScrollY() > child.getBottom() - child.getPaddingBottom() - getHeight()); 1000 | 1001 | } 1002 | 1003 | 1004 | private void onSecondaryPointerUp(MotionEvent ev) 1005 | 1006 | { 1007 | 1008 | final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 1009 | 1010 | final int pointerId = ev.getPointerId(pointerIndex); 1011 | 1012 | if (pointerId == mActivePointerId) 1013 | 1014 | { 1015 | 1016 | // This was our active pointer going up. Choose a new 1017 | 1018 | // active pointer and adjust accordingly. 1019 | 1020 | // TODO: Make this decision more intelligent. 1021 | 1022 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1023 | 1024 | mLastMotionY = ev.getY(newPointerIndex); 1025 | 1026 | mActivePointerId = ev.getPointerId(newPointerIndex); 1027 | 1028 | if (mVelocityTracker != null) 1029 | 1030 | { 1031 | 1032 | mVelocityTracker.clear(); 1033 | 1034 | } 1035 | 1036 | } 1037 | 1038 | } 1039 | 1040 | 1041 | /** 1042 | *
1043 | *
1044 | * Finds the next focusable component that fits in this View's bounds 1045 | *
1046 | * (excluding fading edges) pretending that this View's top is located at 1047 | *
1048 | * the parameter top. 1049 | *
1050 | *
1051 | * 1052 | * @param topFocus look for a candidate is the one at the top of the bounds if 1053 | *1054 | * topFocus is true, or at the bottom of the bounds if topFocus 1055 | *
1056 | * is false 1057 | * @param top the top offset of the bounds in which a focusable must be 1058 | *
1059 | * found (the fading edge is assumed to start at this position) 1060 | * @param preferredFocusable the View that has highest priority and will be returned if it 1061 | *
1062 | * is within my bounds (null is valid) 1063 | * @return the next focusable component in the bounds or null if none can be 1064 | *
1065 | * found 1066 | */ 1067 | 1068 | private View findFocusableViewInMyBounds(final boolean topFocus, final int top, View preferredFocusable) 1069 | 1070 | { 1071 | 1072 | /* 1073 | 1074 | * The fading edge's transparent side should be considered for focus 1075 | 1076 | * since it's mostly visible, so we divide the actual fading edge length 1077 | 1078 | * by 2. 1079 | 1080 | */ 1081 | 1082 | final int fadingEdgeLength = getVerticalFadingEdgeLength() / 2; 1083 | 1084 | final int topWithoutFadingEdge = top + fadingEdgeLength; 1085 | 1086 | final int bottomWithoutFadingEdge = top + getHeight() - fadingEdgeLength; 1087 | 1088 | 1089 | if ((preferredFocusable != null) && (preferredFocusable.getTop() < bottomWithoutFadingEdge) 1090 | 1091 | && (preferredFocusable.getBottom() > topWithoutFadingEdge)) 1092 | 1093 | { 1094 | 1095 | return preferredFocusable; 1096 | 1097 | } 1098 | 1099 | 1100 | return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge); 1101 | 1102 | } 1103 | 1104 | 1105 | /** 1106 | *
1107 | *
1108 | * Finds the next focusable component that fits in the specified bounds. 1109 | *
1110 | *
1111 | * 1112 | * @param topFocus look for a candidate is the one at the top of the bounds if 1113 | *1114 | * topFocus is true, or at the bottom of the bounds if topFocus 1115 | *
1116 | * is false 1117 | * @param top the top offset of the bounds in which a focusable must be 1118 | *
1119 | * found 1120 | * @param bottom the bottom offset of the bounds in which a focusable must be 1121 | *
1122 | * found 1123 | * @return the next focusable component in the bounds or null if none can be 1124 | *
1125 | * found
1126 | */
1127 |
1128 | private View findFocusableViewInBounds(boolean topFocus, int top, int bottom)
1129 |
1130 | {
1131 |
1132 |
1133 | List
1278 | *
1279 | * Handles scrolling in response to a "page up/down" shortcut press. This
1280 | *
1281 | * method will scroll the view by one page up or down and give the focus to
1282 | *
1283 | * the topmost/bottommost component in the new visible area. If no component
1284 | *
1285 | * is a good candidate for focus, this scrollview reclaims the focus.
1286 | *
1287 | *
1291 | * one page up or {@link android.view.View#FOCUS_DOWN} to go one
1292 | *
1293 | * page down
1294 | * @return true if the key event is consumed by this method, false otherwise
1295 | */
1296 |
1297 | public boolean pageScroll(int direction)
1298 |
1299 | {
1300 |
1301 | boolean down = direction == View.FOCUS_DOWN;
1302 |
1303 | int height = getHeight();
1304 |
1305 |
1306 | if (down)
1307 |
1308 | {
1309 |
1310 | mTempRect.top = getScrollY() + height;
1311 |
1312 | int count = getChildCount();
1313 |
1314 | if (count > 0)
1315 |
1316 | {
1317 |
1318 | View view = getChildAt(count - 1);
1319 |
1320 | if (mTempRect.top + height > view.getBottom())
1321 |
1322 | {
1323 |
1324 | mTempRect.top = view.getBottom() - height;
1325 |
1326 | }
1327 |
1328 | }
1329 |
1330 | } else
1331 |
1332 | {
1333 |
1334 | mTempRect.top = getScrollY() - height;
1335 |
1336 | if (mTempRect.top < 0)
1337 |
1338 | {
1339 |
1340 | mTempRect.top = 0;
1341 |
1342 | }
1343 |
1344 | }
1345 |
1346 | mTempRect.bottom = mTempRect.top + height;
1347 |
1348 |
1349 | return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
1350 |
1351 | }
1352 |
1353 |
1354 | /**
1355 | *
1356 | *
1357 | * Handles scrolling in response to a "home/end" shortcut press. This method
1358 | *
1359 | * will scroll the view to the top or bottom and give the focus to the
1360 | *
1361 | * topmost/bottommost component in the new visible area. If no component is
1362 | *
1363 | * a good candidate for focus, this scrollview reclaims the focus.
1364 | *
1365 | *
1369 | * the top of the view or {@link android.view.View#FOCUS_DOWN} to
1370 | *
1371 | * go the bottom
1372 | * @return true if the key event is consumed by this method, false otherwise
1373 | */
1374 |
1375 | public boolean fullScroll(int direction)
1376 |
1377 | {
1378 |
1379 | boolean down = direction == View.FOCUS_DOWN;
1380 |
1381 | int height = getHeight();
1382 |
1383 |
1384 | mTempRect.top = 0;
1385 |
1386 | mTempRect.bottom = height;
1387 |
1388 |
1389 | if (down)
1390 |
1391 | {
1392 |
1393 | int count = getChildCount();
1394 |
1395 | if (count > 0)
1396 |
1397 | {
1398 |
1399 | View view = getChildAt(count - 1);
1400 |
1401 | mTempRect.bottom = view.getBottom();
1402 |
1403 | mTempRect.top = mTempRect.bottom - height;
1404 |
1405 | }
1406 |
1407 | }
1408 |
1409 |
1410 | return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
1411 |
1412 | }
1413 |
1414 |
1415 | /**
1416 | *
1417 | *
1418 | * Scrolls the view to make the area defined by
1420 | *
1422 | * component visible in this area. If no component can be focused in the new
1423 | *
1424 | * visible area, the focus is reclaimed by this scrollview.
1425 | *
1426 | *
1430 | * upward {@link android.view.View#FOCUS_DOWN} to downward
1431 | * @param top the top offset of the new area to be made visible
1432 | * @param bottom the bottom offset of the new area to be made visible
1433 | * @return true if the key event is consumed by this method, false otherwise
1434 | */
1435 |
1436 | private boolean scrollAndFocus(int direction, int top, int bottom)
1437 |
1438 | {
1439 |
1440 | boolean handled = true;
1441 |
1442 |
1443 | int height = getHeight();
1444 |
1445 | int containerTop = getScrollY();
1446 |
1447 | int containerBottom = containerTop + height;
1448 |
1449 | boolean up = direction == View.FOCUS_UP;
1450 |
1451 |
1452 | View newFocused = findFocusableViewInBounds(up, top, bottom);
1453 |
1454 | if (newFocused == null)
1455 |
1456 | {
1457 |
1458 | newFocused = this;
1459 |
1460 | }
1461 |
1462 |
1463 | if (top >= containerTop && bottom <= containerBottom)
1464 |
1465 | {
1466 |
1467 | handled = false;
1468 |
1469 | } else
1470 |
1471 | {
1472 |
1473 | int delta = up ? (top - containerTop) : (bottom - containerBottom);
1474 |
1475 | doScrollY(delta);
1476 |
1477 | }
1478 |
1479 |
1480 | if (newFocused != findFocus() && newFocused.requestFocus(direction))
1481 |
1482 | {
1483 |
1484 | mScrollViewMovedFocus = true;
1485 |
1486 | mScrollViewMovedFocus = false;
1487 |
1488 | }
1489 |
1490 |
1491 | return handled;
1492 |
1493 | }
1494 |
1495 |
1496 | /**
1497 | * Handle scrolling in response to an up or down arrow click.
1498 | *
1499 | * @param direction The direction corresponding to the arrow key that was pressed
1500 | * @return True if we consumed the event, false otherwise
1501 | */
1502 |
1503 | public boolean arrowScroll(int direction)
1504 |
1505 | {
1506 |
1507 |
1508 | View currentFocused = findFocus();
1509 |
1510 | if (currentFocused == this)
1511 |
1512 | currentFocused = null;
1513 |
1514 |
1515 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
1516 |
1517 |
1518 | final int maxJump = getMaxScrollAmount();
1519 |
1520 |
1521 | if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight()))
1522 |
1523 | {
1524 |
1525 | nextFocused.getDrawingRect(mTempRect);
1526 |
1527 | offsetDescendantRectToMyCoords(nextFocused, mTempRect);
1528 |
1529 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1530 |
1531 | doScrollY(scrollDelta);
1532 |
1533 | nextFocused.requestFocus(direction);
1534 |
1535 | } else
1536 |
1537 | {
1538 |
1539 | // no new focus
1540 |
1541 | int scrollDelta = maxJump;
1542 |
1543 |
1544 | if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
1545 |
1546 | {
1547 |
1548 | scrollDelta = getScrollY();
1549 |
1550 | } else if (direction == View.FOCUS_DOWN)
1551 |
1552 | {
1553 |
1554 | if (getChildCount() > 0)
1555 |
1556 | {
1557 |
1558 | int daBottom = getChildAt(0).getBottom();
1559 |
1560 |
1561 | int screenBottom = getScrollY() + getHeight();
1562 |
1563 |
1564 | if (daBottom - screenBottom < maxJump)
1565 |
1566 | {
1567 |
1568 | scrollDelta = daBottom - screenBottom;
1569 |
1570 | }
1571 |
1572 | }
1573 |
1574 | }
1575 |
1576 | if (scrollDelta == 0)
1577 |
1578 | {
1579 |
1580 | return false;
1581 |
1582 | }
1583 |
1584 | doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
1585 |
1586 | }
1587 |
1588 |
1589 | if (currentFocused != null && currentFocused.isFocused() && isOffScreen(currentFocused))
1590 |
1591 | {
1592 |
1593 | // previously focused item still has focus and is off screen, give
1594 |
1595 | // it up (take it back to ourselves)
1596 |
1597 | // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we
1598 |
1599 | // are
1600 |
1601 | // sure to
1602 |
1603 | // get it)
1604 |
1605 | final int descendantFocusability = getDescendantFocusability(); // save
1606 |
1607 | setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
1608 |
1609 | requestFocus();
1610 |
1611 | setDescendantFocusability(descendantFocusability); // restore
1612 |
1613 | }
1614 |
1615 | return true;
1616 |
1617 | }
1618 |
1619 |
1620 | /**
1621 | * @return whether the descendant of this scroll view is scrolled off
1622 | *
1623 | * screen.
1624 | */
1625 |
1626 | private boolean isOffScreen(View descendant)
1627 |
1628 | {
1629 |
1630 | return !isWithinDeltaOfScreen(descendant, 0, getHeight());
1631 |
1632 | }
1633 |
1634 |
1635 | /**
1636 | * @return whether the descendant of this scroll view is within delta pixels
1637 | *
1638 | * of being on the screen.
1639 | */
1640 |
1641 | private boolean isWithinDeltaOfScreen(View descendant, int delta, int height)
1642 |
1643 | {
1644 |
1645 | descendant.getDrawingRect(mTempRect);
1646 |
1647 | offsetDescendantRectToMyCoords(descendant, mTempRect);
1648 |
1649 |
1650 | return (mTempRect.bottom + delta) >= getScrollY() && (mTempRect.top - delta) <= (getScrollY() + height);
1651 |
1652 | }
1653 |
1654 |
1655 | /**
1656 | * Smooth scroll by a Y delta
1657 | *
1658 | * @param delta the number of pixels to scroll by on the Y axis
1659 | */
1660 |
1661 | private void doScrollY(int delta)
1662 |
1663 | {
1664 |
1665 | if (delta != 0)
1666 |
1667 | {
1668 |
1669 | if (mSmoothScrollingEnabled)
1670 |
1671 | {
1672 |
1673 | smoothScrollBy(0, delta);
1674 |
1675 | } else
1676 |
1677 | {
1678 |
1679 | scrollBy(0, delta);
1680 |
1681 | }
1682 |
1683 | }
1684 |
1685 | }
1686 |
1687 |
1688 | /**
1689 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
1690 | *
1691 | * @param dx the number of pixels to scroll by on the X axis
1692 | * @param dy the number of pixels to scroll by on the Y axis
1693 | */
1694 |
1695 | public final void smoothScrollBy(int dx, int dy)
1696 |
1697 | {
1698 |
1699 | if (getChildCount() == 0)
1700 |
1701 | {
1702 |
1703 | // Nothing to do.
1704 |
1705 | return;
1706 |
1707 | }
1708 |
1709 | long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
1710 |
1711 | if (duration > ANIMATED_SCROLL_GAP)
1712 |
1713 | {
1714 |
1715 | final int height = getHeight() - getPaddingBottom() - getPaddingTop();
1716 |
1717 | final int bottom = getChildAt(0).getHeight();
1718 |
1719 | final int maxY = Math.max(0, bottom - height);
1720 |
1721 | final int scrollY = getScrollY();
1722 |
1723 | dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
1724 |
1725 |
1726 | mScroller.startScroll(getScrollX(), scrollY, 0, dy);
1727 |
1728 | invalidate();
1729 |
1730 | } else
1731 |
1732 | {
1733 |
1734 | if (!mScroller.isFinished())
1735 |
1736 | {
1737 |
1738 | mScroller.abortAnimation();
1739 |
1740 | }
1741 |
1742 | scrollBy(dx, dy);
1743 |
1744 | }
1745 |
1746 | mLastScroll = AnimationUtils.currentAnimationTimeMillis();
1747 |
1748 | }
1749 |
1750 |
1751 | public final void smoothScrollToTop()
1752 |
1753 | {
1754 |
1755 | smoothScrollTo(0, child.getPaddingTop());
1756 |
1757 | }
1758 |
1759 |
1760 | public final void smoothScrollToBottom()
1761 |
1762 | {
1763 |
1764 | smoothScrollTo(0, child.getHeight() - child.getPaddingTop() - getHeight());
1765 |
1766 | }
1767 |
1768 |
1769 | /**
1770 | * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
1771 | *
1772 | * @param x the position where to scroll on the X axis
1773 | * @param y the position where to scroll on the Y axis
1774 | */
1775 |
1776 | public final void smoothScrollTo(int x, int y)
1777 |
1778 | {
1779 |
1780 | smoothScrollBy(x - getScrollX(), y - getScrollY());
1781 |
1782 | }
1783 |
1784 |
1785 | /**
1786 | *
1787 | *
1788 | * The scroll range of a scroll view is the overall height of all of its
1789 | *
1790 | * children.
1791 | *
1792 | *
2030 | * first screen size chunk of it) on screen.
2031 | *
2032 | * @param rect The rectangle.
2033 | * @param immediate True to scroll immediately without animation
2034 | * @return true if scrolling was performed
2035 | */
2036 |
2037 | private boolean scrollToChildRect(Rect rect, boolean immediate)
2038 |
2039 | {
2040 |
2041 | final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
2042 |
2043 | final boolean scroll = delta != 0;
2044 |
2045 | if (scroll)
2046 |
2047 | {
2048 |
2049 | if (immediate)
2050 |
2051 | {
2052 |
2053 | scrollBy(0, delta);
2054 |
2055 | } else
2056 |
2057 | {
2058 |
2059 | smoothScrollBy(0, delta);
2060 |
2061 | }
2062 |
2063 | }
2064 |
2065 | return scroll;
2066 |
2067 | }
2068 |
2069 |
2070 | /**
2071 | * Compute the amount to scroll in the Y direction in order to get a
2072 | *
2073 | * rectangle completely on the screen (or, if taller than the screen, at
2074 | *
2075 | * least the first screen size chunk of it).
2076 | *
2077 | * @param rect The rect.
2078 | * @return The scroll delta.
2079 | */
2080 |
2081 | protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect)
2082 |
2083 | {
2084 |
2085 | if (getChildCount() == 0)
2086 |
2087 | return 0;
2088 |
2089 |
2090 | int height = getHeight();
2091 |
2092 | int screenTop = getScrollY();
2093 |
2094 | int screenBottom = screenTop + height;
2095 |
2096 |
2097 | int fadingEdge = getVerticalFadingEdgeLength();
2098 |
2099 |
2100 | // leave room for top fading edge as long as rect isn't at very top
2101 |
2102 | if (rect.top > 0)
2103 |
2104 | {
2105 |
2106 | screenTop += fadingEdge;
2107 |
2108 | }
2109 |
2110 |
2111 | // leave room for bottom fading edge as long as rect isn't at very
2112 |
2113 | // bottom
2114 |
2115 | if (rect.bottom < getChildAt(0).getHeight())
2116 |
2117 | {
2118 |
2119 | screenBottom -= fadingEdge;
2120 |
2121 | }
2122 |
2123 |
2124 | int scrollYDelta = 0;
2125 |
2126 |
2127 | if (rect.bottom > screenBottom && rect.top > screenTop)
2128 |
2129 | {
2130 |
2131 | // need to move down to get it in view: move down just enough so
2132 |
2133 | // that the entire rectangle is in view (or at least the first
2134 |
2135 | // screen size chunk).
2136 |
2137 |
2138 | if (rect.height() > height)
2139 |
2140 | {
2141 |
2142 | // just enough to get screen size chunk on
2143 |
2144 | scrollYDelta += (rect.top - screenTop);
2145 |
2146 | } else
2147 |
2148 | {
2149 |
2150 | // get entire rect at bottom of screen
2151 |
2152 | scrollYDelta += (rect.bottom - screenBottom);
2153 |
2154 | }
2155 |
2156 |
2157 | // make sure we aren't scrolling beyond the end of our content
2158 |
2159 | int bottom = getChildAt(0).getBottom();
2160 |
2161 | int distanceToBottom = bottom - screenBottom;
2162 |
2163 | scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
2164 |
2165 |
2166 | } else if (rect.top < screenTop && rect.bottom < screenBottom)
2167 |
2168 | {
2169 |
2170 | // need to move up to get it in view: move up just enough so that
2171 |
2172 | // entire rectangle is in view (or at least the first screen
2173 |
2174 | // size chunk of it).
2175 |
2176 |
2177 | if (rect.height() > height)
2178 |
2179 | {
2180 |
2181 | // screen size chunk
2182 |
2183 | scrollYDelta -= (screenBottom - rect.bottom);
2184 |
2185 | } else
2186 |
2187 | {
2188 |
2189 | // entire rect at top
2190 |
2191 | scrollYDelta -= (screenTop - rect.top);
2192 |
2193 | }
2194 |
2195 |
2196 | // make sure we aren't scrolling any further than the top our
2197 |
2198 | // content
2199 |
2200 | scrollYDelta = Math.max(scrollYDelta, -getScrollY());
2201 |
2202 | }
2203 |
2204 | return scrollYDelta;
2205 |
2206 | }
2207 |
2208 |
2209 | @Override
2210 |
2211 | public void requestChildFocus(View child, View focused)
2212 |
2213 | {
2214 |
2215 | if (!mScrollViewMovedFocus)
2216 |
2217 | {
2218 |
2219 | if (!mIsLayoutDirty)
2220 |
2221 | {
2222 |
2223 | scrollToChild(focused);
2224 |
2225 | } else
2226 |
2227 | {
2228 |
2229 | // The child may not be laid out yet, we can't compute the
2230 |
2231 | // scroll yet
2232 |
2233 | mChildToScrollTo = focused;
2234 |
2235 | }
2236 |
2237 | }
2238 |
2239 | super.requestChildFocus(child, focused);
2240 |
2241 | }
2242 |
2243 |
2244 | /**
2245 | * When looking for focus in children of a scroll view, need to be a little
2246 | *
2247 | * more careful not to give focus to something that is scrolled off screen.
2248 | *
2249 | *
2250 | *
2251 | * This is more expensive than the default {@link android.view.ViewGroup}
2252 | *
2253 | * implementation, otherwise this behavior might have been made the default.
2254 | */
2255 |
2256 | @Override
2257 |
2258 | protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)
2259 |
2260 | {
2261 |
2262 |
2263 | // convert from forward / backward notation to up / down / left / right
2264 |
2265 | // (ugh).
2266 |
2267 | if (direction == View.FOCUS_FORWARD)
2268 |
2269 | {
2270 |
2271 | direction = View.FOCUS_DOWN;
2272 |
2273 | } else if (direction == View.FOCUS_BACKWARD)
2274 |
2275 | {
2276 |
2277 | direction = View.FOCUS_UP;
2278 |
2279 | }
2280 |
2281 |
2282 | final View nextFocus = previouslyFocusedRect == null ? FocusFinder.getInstance().findNextFocus(this, null, direction) : FocusFinder
2283 |
2284 | .getInstance().findNextFocusFromRect(this, previouslyFocusedRect, direction);
2285 |
2286 |
2287 | if (nextFocus == null)
2288 |
2289 | {
2290 |
2291 | return false;
2292 |
2293 | }
2294 |
2295 |
2296 | if (isOffScreen(nextFocus))
2297 |
2298 | {
2299 |
2300 | return false;
2301 |
2302 | }
2303 |
2304 |
2305 | return nextFocus.requestFocus(direction, previouslyFocusedRect);
2306 |
2307 | }
2308 |
2309 |
2310 | @Override
2311 |
2312 | public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)
2313 |
2314 | {
2315 |
2316 | // offset into coordinate space of this scroll view
2317 |
2318 | rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
2319 |
2320 |
2321 | return scrollToChildRect(rectangle, immediate);
2322 |
2323 | }
2324 |
2325 |
2326 | @Override
2327 |
2328 | public void requestLayout()
2329 |
2330 | {
2331 |
2332 | mIsLayoutDirty = true;
2333 |
2334 | super.requestLayout();
2335 |
2336 | }
2337 |
2338 |
2339 | @Override
2340 |
2341 | protected void onLayout(boolean changed, int l, int t, int r, int b)
2342 |
2343 | {
2344 |
2345 | super.onLayout(changed, l, t, r, b);
2346 |
2347 | mIsLayoutDirty = false;
2348 |
2349 | // Give a child focus if it needs it
2350 |
2351 | if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this))
2352 |
2353 | {
2354 |
2355 | scrollToChild(mChildToScrollTo);
2356 |
2357 | }
2358 |
2359 | mChildToScrollTo = null;
2360 |
2361 |
2362 | // Calling this with the present values causes it to re-clam them
2363 |
2364 | scrollTo(getScrollX(), getScrollY());
2365 |
2366 | }
2367 |
2368 |
2369 | @Override
2370 |
2371 | protected void onSizeChanged(int w, int h, int oldw, int oldh)
2372 |
2373 | {
2374 |
2375 | super.onSizeChanged(w, h, oldw, oldh);
2376 |
2377 |
2378 | View currentFocused = findFocus();
2379 |
2380 | if (null == currentFocused || this == currentFocused)
2381 |
2382 | return;
2383 |
2384 |
2385 | // If the currently-focused view was visible on the screen when the
2386 |
2387 | // screen was at the old height, then scroll the screen to make that
2388 |
2389 | // view visible with the new screen height.
2390 |
2391 | if (isWithinDeltaOfScreen(currentFocused, 0, oldh))
2392 |
2393 | {
2394 |
2395 | currentFocused.getDrawingRect(mTempRect);
2396 |
2397 | offsetDescendantRectToMyCoords(currentFocused, mTempRect);
2398 |
2399 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
2400 |
2401 | doScrollY(scrollDelta);
2402 |
2403 | }
2404 |
2405 | }
2406 |
2407 |
2408 | @Override
2409 |
2410 | protected void onScrollChanged(int leftOfVisibleView, int topOfVisibleView, int oldLeftOfVisibleView, int oldTopOfVisibleView)
2411 |
2412 | {
2413 |
2414 | int displayHeight = getHeight();
2415 |
2416 | int paddingTop = child.getPaddingTop();
2417 |
2418 | int contentBottom = child.getHeight() - child.getPaddingBottom();
2419 |
2420 |
2421 | if (isInFlingMode)
2422 |
2423 | {
2424 |
2425 |
2426 | if (topOfVisibleView < paddingTop || topOfVisibleView > contentBottom - displayHeight)
2427 |
2428 | {
2429 |
2430 | if (topOfVisibleView < paddingTop)
2431 |
2432 | {
2433 |
2434 | mScroller.startScroll(0, topOfVisibleView, 0, paddingTop - topOfVisibleView, 1000);
2435 |
2436 | } else if (topOfVisibleView > contentBottom - displayHeight)
2437 |
2438 | {
2439 |
2440 | mScroller.startScroll(0, topOfVisibleView, 0, contentBottom - displayHeight - topOfVisibleView, 1000);
2441 |
2442 | }
2443 |
2444 |
2445 | // Start animation.
2446 |
2447 | post(overScrollerSpringbackTask);
2448 |
2449 | isInFlingMode = false;
2450 |
2451 | return;
2452 |
2453 |
2454 | }
2455 |
2456 | }
2457 |
2458 | super.onScrollChanged(leftOfVisibleView, topOfVisibleView, oldLeftOfVisibleView, oldTopOfVisibleView);
2459 |
2460 | }
2461 |
2462 |
2463 | /**
2464 | * Return true if child is an descendant of parent, (or equal to the
2465 | *
2466 | * parent).
2467 | */
2468 |
2469 | private boolean isViewDescendantOf(View child, View parent)
2470 |
2471 | {
2472 |
2473 | if (child == parent)
2474 |
2475 | {
2476 |
2477 | return true;
2478 |
2479 | }
2480 |
2481 |
2482 | final ViewParent theParent = child.getParent();
2483 |
2484 | return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
2485 |
2486 | }
2487 |
2488 |
2489 | /**
2490 | * Fling the scroll view
2491 | *
2492 | * @param velocityY The initial velocity in the Y direction. Positive numbers mean
2493 | *
2494 | * that the finger/cursor is moving down the screen, which means
2495 | *
2496 | * we want to scroll towards the top.
2497 | */
2498 |
2499 | public void fling(int velocityY)
2500 |
2501 | {
2502 |
2503 | if (getChildCount() > 0)
2504 |
2505 | {
2506 |
2507 | int height = getHeight() - getPaddingBottom() - getPaddingTop();
2508 |
2509 | int bottom = getChildAt(0).getHeight();
2510 |
2511 |
2512 | mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, Math.max(0, bottom - height));
2513 |
2514 |
2515 | final boolean movingDown = velocityY > 0;
2516 |
2517 |
2518 | View newFocused = findFocusableViewInMyBounds(movingDown, mScroller.getFinalY(), findFocus());
2519 |
2520 | if (newFocused == null)
2521 |
2522 | {
2523 |
2524 | newFocused = this;
2525 |
2526 | }
2527 |
2528 |
2529 | if (newFocused != findFocus() && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP))
2530 |
2531 | {
2532 |
2533 | mScrollViewMovedFocus = true;
2534 |
2535 | mScrollViewMovedFocus = false;
2536 |
2537 | }
2538 |
2539 |
2540 | invalidate();
2541 |
2542 | }
2543 |
2544 | }
2545 |
2546 |
2547 | /**
2548 | * {@inheritDoc}
2549 | *
2550 | *
2551 | *
2552 | *
2553 | *
2554 | * This version also clamps the scrolling to the bounds of our child.
2555 | */
2556 |
2557 | @Override
2558 |
2559 | public void scrollTo(int x, int y)
2560 |
2561 | {
2562 |
2563 | // we rely on the fact the View.scrollBy calls scrollTo.
2564 |
2565 | if (getChildCount() > 0)
2566 |
2567 | {
2568 |
2569 | View child = getChildAt(0);
2570 |
2571 | x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
2572 |
2573 | y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
2574 |
2575 | if (x != getScrollX() || y != getScrollY())
2576 |
2577 | {
2578 |
2579 | super.scrollTo(x, y);
2580 |
2581 | }
2582 |
2583 | }
2584 |
2585 | }
2586 |
2587 |
2588 | private int clamp(int n, int my, int child)
2589 |
2590 | {
2591 |
2592 | if (my >= child || n < 0)
2593 |
2594 | {
2595 |
2596 | /*
2597 |
2598 | * my >= child is this case: |--------------- me ---------------|
2599 |
2600 | * |------ child ------| or |--------------- me ---------------|
2601 |
2602 | * |------ child ------| or |--------------- me ---------------|
2603 |
2604 | * |------ child ------|
2605 |
2606 | *
2607 |
2608 | * n < 0 is this case: |------ me ------| |-------- child --------|
2609 |
2610 | * |-- getScrollX() --|
2611 |
2612 | */
2613 |
2614 | return 0;
2615 |
2616 | }
2617 |
2618 | if ((my + n) > child) {
2619 | /*
2620 | * this case: |------ me ------| |------ child ------| |--
2621 | * getScrollX() --|
2622 | */
2623 | return child - my;
2624 | }
2625 | return n;
2626 | }
2627 |
2628 | @Override
2629 | public boolean onTouch(View v, MotionEvent event) {
2630 | // Stop scrolling calculation.
2631 | mScroller.forceFinished(true);
2632 | // Stop scrolling animation.
2633 | removeCallbacks(overScrollerSpringbackTask);
2634 | if (event.getAction() == MotionEvent.ACTION_UP) {
2635 | return overScrollView();
2636 | } else if (event.getAction() == MotionEvent.ACTION_CANCEL) {
2637 | return overScrollView();
2638 | }
2639 | return false;
2640 | }
2641 |
2642 | private boolean overScrollView() {
2643 | // The height of scroll view, in pixels
2644 | int displayHeight = getHeight();
2645 | // The top of content view, in pixels.
2646 | int contentTop = child.getPaddingTop();
2647 | // The top of content view, in pixels.
2648 | int contentBottom = child.getHeight() - child.getPaddingBottom();
2649 | // The scrolled top position of scroll view, in pixels.
2650 | int currScrollY = getScrollY();
2651 | int scrollBy;
2652 | // Scroll to content top
2653 | if (currScrollY < contentTop) {
2654 | onOverScroll(currScrollY);
2655 | scrollBy = contentTop - currScrollY;
2656 | } else if (currScrollY + displayHeight > contentBottom) {
2657 | // Scroll to content top
2658 | if (child.getHeight() - child.getPaddingTop() - child.getPaddingBottom() < displayHeight) {
2659 | scrollBy = contentTop - currScrollY;
2660 | }
2661 | // Scroll to content bottom
2662 | else {
2663 | scrollBy = contentBottom - displayHeight - currScrollY;
2664 | // Log.d(Definitions.LOG_TAG, "scrollBy=" + scrollBy);
2665 | }
2666 | // fire onOverScroll event, and update scrollBy if a loadingView has
2667 | // been added to the scroller.
2668 | scrollBy += onOverScroll(currScrollY);
2669 | }
2670 | // scrolling between the contentTop and contentBottom
2671 | else {
2672 | isInFlingMode = true;
2673 | return false;
2674 | }
2675 | mScroller.startScroll(0, currScrollY, 0, scrollBy, 500);
2676 | // Start animation.
2677 | post(overScrollerSpringbackTask);
2678 | prevScrollY = currScrollY;
2679 | // consume(to stop fling)
2680 | return true;
2681 | }
2682 |
2683 | protected int onOverScroll(int scrollY) {
2684 | return 0;
2685 | }
2686 | }
2687 |
2688 |
--------------------------------------------------------------------------------
top and
1419 | * bottom visible. This method attempts to give the focus to a
1421 | *