├── README.md └── ReboundScrollView.java /README.md: -------------------------------------------------------------------------------- 1 | # ReboundScrollView 2 | 完美的阻尼效果类 弹性效果类 可以嵌套任意View 自带滚动的View也可以 跟左右滑动0冲突 3 | 4 | 使用方法: 5 | 6 | 你要嵌套的view 7 | 8 | 9 | 示例: 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ReboundScrollView.java: -------------------------------------------------------------------------------- 1 | import android.content.Context; 2 | import android.graphics.Rect; 3 | import android.util.AttributeSet; 4 | import android.util.DisplayMetrics; 5 | import android.view.FocusFinder; 6 | import android.view.KeyEvent; 7 | import android.view.LayoutInflater; 8 | import android.view.MotionEvent; 9 | import android.view.VelocityTracker; 10 | import android.view.View; 11 | import android.view.ViewConfiguration; 12 | import android.view.ViewGroup; 13 | import android.view.ViewParent; 14 | import android.view.animation.AnimationUtils; 15 | import android.view.animation.OvershootInterpolator; 16 | import android.widget.FrameLayout; 17 | import android.widget.Scroller; 18 | import android.view.View.OnTouchListener; 19 | 20 | import java.lang.reflect.Field; 21 | import java.util.List; 22 | 23 | public class ReboundScrollView extends FrameLayout implements OnTouchListener { 24 | 25 | static final int ANIMATED_SCROLL_GAP = 250; 26 | static final float MAX_SCROLL_FACTOR = 0.5f; 27 | static final float OVERSHOOT_TENSION = 0.75f; 28 | private long mLastScroll; 29 | private final Rect mTempRect = new Rect(); 30 | private Scroller mScroller; 31 | protected Context mContext; 32 | Field mScrollYField; 33 | Field mScrollXField; 34 | boolean hasFailedObtainingScrollFields; 35 | int prevScrollY; 36 | boolean isInFlingMode = false; 37 | DisplayMetrics metrics; 38 | LayoutInflater inflater; 39 | protected View child; 40 | private Runnable overScrollerSpringbackTask; 41 | 42 | /** 43 | * Flag to indicate that we are moving focus ourselves. This is so the code 44 | *

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 focusables = getFocusables(View.FOCUS_FORWARD); 1134 | 1135 | View focusCandidate = null; 1136 | 1137 | 1138 | 1139 | /* 1140 | 1141 | * A fully contained focusable is one where its top is below the bound's 1142 | 1143 | * top, and its bottom is above the bound's bottom. A partially 1144 | 1145 | * contained focusable is one where some part of it is within the 1146 | 1147 | * bounds, but it also has some part that is not within bounds. A fully 1148 | 1149 | * contained focusable is preferred to a partially contained focusable. 1150 | 1151 | */ 1152 | 1153 | boolean foundFullyContainedFocusable = false; 1154 | 1155 | 1156 | int count = focusables.size(); 1157 | 1158 | for (int i = 0; i < count; i++) 1159 | 1160 | { 1161 | 1162 | View view = focusables.get(i); 1163 | 1164 | int viewTop = view.getTop(); 1165 | 1166 | int viewBottom = view.getBottom(); 1167 | 1168 | 1169 | if (top < viewBottom && viewTop < bottom) 1170 | 1171 | { 1172 | 1173 | /* 1174 | 1175 | * the focusable is in the target area, it is a candidate for 1176 | 1177 | * focusing 1178 | 1179 | */ 1180 | 1181 | 1182 | final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom); 1183 | 1184 | 1185 | if (focusCandidate == null) 1186 | 1187 | { 1188 | 1189 | /* No candidate, take this one */ 1190 | 1191 | focusCandidate = view; 1192 | 1193 | foundFullyContainedFocusable = viewIsFullyContained; 1194 | 1195 | } else 1196 | 1197 | { 1198 | 1199 | final boolean viewIsCloserToBoundary = (topFocus && viewTop < focusCandidate.getTop()) 1200 | 1201 | || (!topFocus && viewBottom > focusCandidate.getBottom()); 1202 | 1203 | 1204 | if (foundFullyContainedFocusable) 1205 | 1206 | { 1207 | 1208 | if (viewIsFullyContained && viewIsCloserToBoundary) 1209 | 1210 | { 1211 | 1212 | /* 1213 | 1214 | * We're dealing with only fully contained views, so 1215 | 1216 | * it has to be closer to the boundary to beat our 1217 | 1218 | * candidate 1219 | 1220 | */ 1221 | 1222 | focusCandidate = view; 1223 | 1224 | } 1225 | 1226 | } else 1227 | 1228 | { 1229 | 1230 | if (viewIsFullyContained) 1231 | 1232 | { 1233 | 1234 | /* 1235 | 1236 | * Any fully contained view beats a partially 1237 | 1238 | * contained view 1239 | 1240 | */ 1241 | 1242 | focusCandidate = view; 1243 | 1244 | foundFullyContainedFocusable = true; 1245 | 1246 | } else if (viewIsCloserToBoundary) 1247 | 1248 | { 1249 | 1250 | /* 1251 | 1252 | * Partially contained view beats another partially 1253 | 1254 | * contained view if it's closer 1255 | 1256 | */ 1257 | 1258 | focusCandidate = view; 1259 | 1260 | } 1261 | 1262 | } 1263 | 1264 | } 1265 | 1266 | } 1267 | 1268 | } 1269 | 1270 | 1271 | return focusCandidate; 1272 | 1273 | } 1274 | 1275 | 1276 | /** 1277 | *

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 | *

1288 | * 1289 | * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} to go 1290 | *

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 | *

1366 | * 1367 | * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} to go 1368 | *

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 top and 1419 | *

1420 | * bottom visible. This method attempts to give the focus to a 1421 | *

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 | *

1427 | * 1428 | * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} to go 1429 | *

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 | *

1793 | */ 1794 | 1795 | @Override 1796 | 1797 | protected int computeVerticalScrollRange() 1798 | 1799 | { 1800 | 1801 | final int count = getChildCount(); 1802 | 1803 | final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); 1804 | 1805 | if (count == 0) 1806 | 1807 | { 1808 | 1809 | return contentHeight; 1810 | 1811 | } 1812 | 1813 | 1814 | return getChildAt(0).getBottom(); 1815 | 1816 | } 1817 | 1818 | 1819 | @Override 1820 | 1821 | protected int computeVerticalScrollOffset() 1822 | 1823 | { 1824 | 1825 | return Math.max(0, super.computeVerticalScrollOffset()); 1826 | 1827 | } 1828 | 1829 | 1830 | @Override 1831 | 1832 | protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) 1833 | 1834 | { 1835 | 1836 | ViewGroup.LayoutParams lp = child.getLayoutParams(); 1837 | 1838 | 1839 | int childWidthMeasureSpec; 1840 | 1841 | int childHeightMeasureSpec; 1842 | 1843 | 1844 | childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width); 1845 | 1846 | 1847 | childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1848 | 1849 | 1850 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1851 | 1852 | } 1853 | 1854 | 1855 | @Override 1856 | 1857 | protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, 1858 | 1859 | int heightUsed) 1860 | 1861 | { 1862 | 1863 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 1864 | 1865 | 1866 | final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin 1867 | 1868 | + lp.rightMargin + widthUsed, lp.width); 1869 | 1870 | final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); 1871 | 1872 | 1873 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1874 | 1875 | } 1876 | 1877 | 1878 | @Override 1879 | 1880 | public void computeScroll() 1881 | 1882 | { 1883 | 1884 | // If android implementation has changed and we cannot obtain mScrollY - 1885 | 1886 | // The default behavior will be applied by the parent. 1887 | 1888 | if (hasFailedObtainingScrollFields) 1889 | 1890 | { 1891 | 1892 | super.computeScroll(); 1893 | 1894 | return; 1895 | 1896 | } 1897 | 1898 | 1899 | if (mScroller.computeScrollOffset()) 1900 | 1901 | { 1902 | 1903 | // This is called at drawing time by ViewGroup. We don't want to 1904 | 1905 | // re-show the scrollbars at this point, which scrollTo will do, 1906 | 1907 | // so we replicate most of scrollTo here. 1908 | 1909 | // 1910 | 1911 | // It's a little odd to call onScrollChanged from inside the 1912 | 1913 | // drawing. 1914 | 1915 | // 1916 | 1917 | // It is, except when you remember that computeScroll() is used to 1918 | 1919 | // animate scrolling. So unless we want to defer the 1920 | 1921 | // onScrollChanged() 1922 | 1923 | // until the end of the animated scrolling, we don't really have a 1924 | 1925 | // choice here. 1926 | 1927 | // 1928 | 1929 | // I agree. The alternative, which I think would be worse, is to 1930 | 1931 | // post 1932 | 1933 | // something and tell the subclasses later. This is bad because 1934 | 1935 | // there 1936 | 1937 | // will be a window where getScrollX()/Y is different from what the 1938 | 1939 | // app 1940 | 1941 | // thinks it is. 1942 | 1943 | // 1944 | 1945 | int oldX = getScrollX(); 1946 | 1947 | int oldY = getScrollY(); 1948 | 1949 | int x = mScroller.getCurrX(); 1950 | 1951 | int y = mScroller.getCurrY(); 1952 | 1953 | 1954 | if (getChildCount() > 0) 1955 | 1956 | { 1957 | 1958 | View child = getChildAt(0); 1959 | 1960 | x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); 1961 | 1962 | y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); 1963 | 1964 | if (x != oldX || y != oldY) 1965 | 1966 | { 1967 | 1968 | SetScrollX(x); 1969 | 1970 | // mScrollX = x; 1971 | 1972 | SetScrollY(y); 1973 | 1974 | // mScrollY = y; 1975 | 1976 | onScrollChanged(x, y, oldX, oldY); 1977 | 1978 | } 1979 | 1980 | } 1981 | 1982 | awakenScrollBars(); 1983 | 1984 | 1985 | // Keep on drawing until the animation has finished. 1986 | 1987 | postInvalidate(); 1988 | 1989 | } 1990 | 1991 | } 1992 | 1993 | 1994 | /** 1995 | * Scrolls the view to the given child. 1996 | * 1997 | * @param child the View to scroll to 1998 | */ 1999 | 2000 | private void scrollToChild(View child) 2001 | 2002 | { 2003 | 2004 | child.getDrawingRect(mTempRect); 2005 | 2006 | 2007 | 2008 | /* Offset from child's local coordinates to ScrollView coordinates */ 2009 | 2010 | offsetDescendantRectToMyCoords(child, mTempRect); 2011 | 2012 | 2013 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 2014 | 2015 | 2016 | if (scrollDelta != 0) 2017 | 2018 | { 2019 | 2020 | scrollBy(0, scrollDelta); 2021 | 2022 | } 2023 | 2024 | } 2025 | 2026 | 2027 | /** 2028 | * If rect is off screen, scroll just enough to get it (or at least the 2029 | *

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 | --------------------------------------------------------------------------------