├── .gitignore
├── .idea
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── encodings.xml
├── gradle.xml
├── inspectionProfiles
│ ├── My.xml
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── shizhefei
│ │ └── view
│ │ └── hvscrollview
│ │ └── demo
│ │ ├── CompareActivity.java
│ │ ├── GuideActivity.java
│ │ ├── MainActivity.java
│ │ └── MoreLayerScrollViewActivity.java
│ └── res
│ ├── drawable-xhdpi
│ ├── edit_normal.9.png
│ └── edit_select.9.png
│ ├── drawable
│ └── edittext_bg.xml
│ ├── layout
│ ├── activity_compare.xml
│ ├── activity_guide.xml
│ ├── activity_main.xml
│ ├── activity_more_layer_scroll_view.xml
│ ├── empty.xml
│ ├── item.xml
│ ├── item_toptab.xml
│ ├── layout2.xml
│ ├── scroll_1.xml
│ ├── scroll_2.xml
│ ├── scroll_3.xml
│ └── tttt.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.png
│ ├── values-w820dp
│ └── dimens.xml
│ └── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── shizhefei
│ │ └── view
│ │ └── hvscrollview
│ │ └── HVScrollView.java
│ └── res
│ └── values
│ └── hvscrollview_attrs.xml
├── raw
├── HVScrollView.apk
├── HVScrollView.gif
└── NestedScrollView.java
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | /local.properties
3 | /.idea/
4 | /gradle.properties
5 | /gradlew
6 | /gradlew.bat
7 | gradle/
8 | build/
9 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
17 |
22 |
27 |
This version of the interface works on all versions of Android, back to API v4.
76 | * 77 | * @see #setOnScrollChangeListener(OnScrollChangeListener) 78 | */ 79 | public interface OnScrollChangeListener { 80 | /** 81 | * Called when the scroll position of a view changes. 82 | * 83 | * @param v The view whose scroll position has changed. 84 | * @param scrollX Current horizontal scroll origin. 85 | * @param scrollY Current vertical scroll origin. 86 | * @param oldScrollX Previous horizontal scroll origin. 87 | * @param oldScrollY Previous vertical scroll origin. 88 | */ 89 | void onScrollChange(NestedScrollView v, int scrollX, int scrollY, 90 | int oldScrollX, int oldScrollY); 91 | } 92 | 93 | private long mLastScroll; 94 | 95 | private final Rect mTempRect = new Rect(); 96 | private ScrollerCompat mScroller; 97 | private EdgeEffectCompat mEdgeGlowTop; 98 | private EdgeEffectCompat mEdgeGlowBottom; 99 | 100 | /** 101 | * Position of the last motion event. 102 | */ 103 | private int mLastMotionY; 104 | 105 | /** 106 | * True when the layout has changed but the traversal has not come through yet. 107 | * Ideally the view hierarchy would keep track of this for us. 108 | */ 109 | private boolean mIsLayoutDirty = true; 110 | private boolean mIsLaidOut = false; 111 | 112 | /** 113 | * The child to give focus to in the event that a child has requested focus while the 114 | * layout is dirty. This prevents the scroll from being wrong if the child has not been 115 | * laid out before requesting focus. 116 | */ 117 | private View mChildToScrollTo = null; 118 | 119 | /** 120 | * True if the user is currently dragging this ScrollView around. This is 121 | * not the same as 'is being flinged', which can be checked by 122 | * mScroller.isFinished() (flinging begins when the user lifts his finger). 123 | */ 124 | private boolean mIsBeingDragged = false; 125 | 126 | /** 127 | * Determines speed during touch scrolling 128 | */ 129 | private VelocityTracker mVelocityTracker; 130 | 131 | /** 132 | * When set to true, the scroll view measure its child to make it fill the currently 133 | * visible area. 134 | */ 135 | private boolean mFillViewport; 136 | 137 | /** 138 | * Whether arrow scrolling is animated. 139 | */ 140 | private boolean mSmoothScrollingEnabled = true; 141 | 142 | private int mTouchSlop; 143 | private int mMinimumVelocity; 144 | private int mMaximumVelocity; 145 | 146 | /** 147 | * ID of the active pointer. This is used to retain consistency during 148 | * drags/flings if multiple pointers are used. 149 | */ 150 | private int mActivePointerId = INVALID_POINTER; 151 | 152 | /** 153 | * Used during scrolling to retrieve the new offset within the window. 154 | */ 155 | private final int[] mScrollOffset = new int[2]; 156 | private final int[] mScrollConsumed = new int[2]; 157 | private int mNestedYOffset; 158 | 159 | /** 160 | * Sentinel value for no current active pointer. 161 | * Used by {@link #mActivePointerId}. 162 | */ 163 | private static final int INVALID_POINTER = -1; 164 | 165 | private SavedState mSavedState; 166 | 167 | private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); 168 | 169 | private static final int[] SCROLLVIEW_STYLEABLE = new int[] { 170 | android.R.attr.fillViewport 171 | }; 172 | 173 | private final NestedScrollingParentHelper mParentHelper; 174 | private final NestedScrollingChildHelper mChildHelper; 175 | 176 | private float mVerticalScrollFactor; 177 | 178 | private OnScrollChangeListener mOnScrollChangeListener; 179 | 180 | public NestedScrollView(Context context) { 181 | this(context, null); 182 | } 183 | 184 | public NestedScrollView(Context context, AttributeSet attrs) { 185 | this(context, attrs, 0); 186 | } 187 | 188 | public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) { 189 | super(context, attrs, defStyleAttr); 190 | initScrollView(); 191 | 192 | final TypedArray a = context.obtainStyledAttributes( 193 | attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); 194 | 195 | setFillViewport(a.getBoolean(0, false)); 196 | 197 | a.recycle(); 198 | 199 | mParentHelper = new NestedScrollingParentHelper(this); 200 | mChildHelper = new NestedScrollingChildHelper(this); 201 | 202 | // ...because why else would you be using this widget? 203 | setNestedScrollingEnabled(true); 204 | 205 | ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); 206 | } 207 | 208 | // NestedScrollingChild 209 | 210 | @Override 211 | public void setNestedScrollingEnabled(boolean enabled) { 212 | mChildHelper.setNestedScrollingEnabled(enabled); 213 | } 214 | 215 | @Override 216 | public boolean isNestedScrollingEnabled() { 217 | return mChildHelper.isNestedScrollingEnabled(); 218 | } 219 | 220 | @Override 221 | public boolean startNestedScroll(int axes) { 222 | return mChildHelper.startNestedScroll(axes); 223 | } 224 | 225 | @Override 226 | public void stopNestedScroll() { 227 | mChildHelper.stopNestedScroll(); 228 | } 229 | 230 | @Override 231 | public boolean hasNestedScrollingParent() { 232 | return mChildHelper.hasNestedScrollingParent(); 233 | } 234 | 235 | @Override 236 | public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, 237 | int dyUnconsumed, int[] offsetInWindow) { 238 | return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, 239 | offsetInWindow); 240 | } 241 | 242 | @Override 243 | public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { 244 | return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); 245 | } 246 | 247 | @Override 248 | public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { 249 | return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); 250 | } 251 | 252 | @Override 253 | public boolean dispatchNestedPreFling(float velocityX, float velocityY) { 254 | return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); 255 | } 256 | 257 | // NestedScrollingParent 258 | 259 | @Override 260 | public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 261 | return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; 262 | } 263 | 264 | @Override 265 | public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { 266 | mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); 267 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 268 | } 269 | 270 | @Override 271 | public void onStopNestedScroll(View target) { 272 | mParentHelper.onStopNestedScroll(target); 273 | stopNestedScroll(); 274 | } 275 | 276 | @Override 277 | public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, 278 | int dyUnconsumed) { 279 | final int oldScrollY = getScrollY(); 280 | scrollBy(0, dyUnconsumed); 281 | final int myConsumed = getScrollY() - oldScrollY; 282 | final int myUnconsumed = dyUnconsumed - myConsumed; 283 | dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); 284 | } 285 | 286 | @Override 287 | public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 288 | dispatchNestedPreScroll(dx, dy, consumed, null); 289 | } 290 | 291 | @Override 292 | public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 293 | if (!consumed) { 294 | flingWithNestedDispatch((int) velocityY); 295 | return true; 296 | } 297 | return false; 298 | } 299 | 300 | @Override 301 | public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 302 | return dispatchNestedPreFling(velocityX, velocityY); 303 | } 304 | 305 | @Override 306 | public int getNestedScrollAxes() { 307 | return mParentHelper.getNestedScrollAxes(); 308 | } 309 | 310 | // ScrollView import 311 | 312 | public boolean shouldDelayChildPressedState() { 313 | return true; 314 | } 315 | 316 | @Override 317 | protected float getTopFadingEdgeStrength() { 318 | if (getChildCount() == 0) { 319 | return 0.0f; 320 | } 321 | 322 | final int length = getVerticalFadingEdgeLength(); 323 | final int scrollY = getScrollY(); 324 | if (scrollY < length) { 325 | return scrollY / (float) length; 326 | } 327 | 328 | return 1.0f; 329 | } 330 | 331 | @Override 332 | protected float getBottomFadingEdgeStrength() { 333 | if (getChildCount() == 0) { 334 | return 0.0f; 335 | } 336 | 337 | final int length = getVerticalFadingEdgeLength(); 338 | final int bottomEdge = getHeight() - getPaddingBottom(); 339 | final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; 340 | if (span < length) { 341 | return span / (float) length; 342 | } 343 | 344 | return 1.0f; 345 | } 346 | 347 | /** 348 | * @return The maximum amount this scroll view will scroll in response to 349 | * an arrow event. 350 | */ 351 | public int getMaxScrollAmount() { 352 | return (int) (MAX_SCROLL_FACTOR * getHeight()); 353 | } 354 | 355 | private void initScrollView() { 356 | mScroller = ScrollerCompat.create(getContext(), null); 357 | setFocusable(true); 358 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 359 | setWillNotDraw(false); 360 | final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 361 | mTouchSlop = configuration.getScaledTouchSlop(); 362 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 363 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 364 | } 365 | 366 | @Override 367 | public void addView(View child) { 368 | if (getChildCount() > 0) { 369 | throw new IllegalStateException("ScrollView can host only one direct child"); 370 | } 371 | 372 | super.addView(child); 373 | } 374 | 375 | @Override 376 | public void addView(View child, int index) { 377 | if (getChildCount() > 0) { 378 | throw new IllegalStateException("ScrollView can host only one direct child"); 379 | } 380 | 381 | super.addView(child, index); 382 | } 383 | 384 | @Override 385 | public void addView(View child, ViewGroup.LayoutParams params) { 386 | if (getChildCount() > 0) { 387 | throw new IllegalStateException("ScrollView can host only one direct child"); 388 | } 389 | 390 | super.addView(child, params); 391 | } 392 | 393 | @Override 394 | public void addView(View child, int index, ViewGroup.LayoutParams params) { 395 | if (getChildCount() > 0) { 396 | throw new IllegalStateException("ScrollView can host only one direct child"); 397 | } 398 | 399 | super.addView(child, index, params); 400 | } 401 | 402 | /** 403 | * Register a callback to be invoked when the scroll X or Y positions of 404 | * this view change. 405 | *This version of the method works on all versions of Android, back to API v4.
406 | * 407 | * @param l The listener to notify when the scroll X or Y position changes. 408 | * @see android.view.View#getScrollX() 409 | * @see android.view.View#getScrollY() 410 | */ 411 | public void setOnScrollChangeListener(OnScrollChangeListener l) { 412 | mOnScrollChangeListener = l; 413 | } 414 | 415 | /** 416 | * @return Returns true this ScrollView can be scrolled 417 | */ 418 | private boolean canScroll() { 419 | View child = getChildAt(0); 420 | if (child != null) { 421 | int childHeight = child.getHeight(); 422 | return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); 423 | } 424 | return false; 425 | } 426 | 427 | /** 428 | * Indicates whether this ScrollView's content is stretched to fill the viewport. 429 | * 430 | * @return True if the content fills the viewport, false otherwise. 431 | * 432 | * @attr name android:fillViewport 433 | */ 434 | public boolean isFillViewport() { 435 | return mFillViewport; 436 | } 437 | 438 | /** 439 | * Indicates this ScrollView whether it should stretch its content height to fill 440 | * the viewport or not. 441 | * 442 | * @param fillViewport True to stretch the content's height to the viewport's 443 | * boundaries, false otherwise. 444 | * 445 | * @attr name android:fillViewport 446 | */ 447 | public void setFillViewport(boolean fillViewport) { 448 | if (fillViewport != mFillViewport) { 449 | mFillViewport = fillViewport; 450 | requestLayout(); 451 | } 452 | } 453 | 454 | /** 455 | * @return Whether arrow scrolling will animate its transition. 456 | */ 457 | public boolean isSmoothScrollingEnabled() { 458 | return mSmoothScrollingEnabled; 459 | } 460 | 461 | /** 462 | * Set whether arrow scrolling will animate its transition. 463 | * @param smoothScrollingEnabled whether arrow scrolling will animate its transition 464 | */ 465 | public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { 466 | mSmoothScrollingEnabled = smoothScrollingEnabled; 467 | } 468 | 469 | @Override 470 | protected void onScrollChanged(int l, int t, int oldl, int oldt) { 471 | super.onScrollChanged(l, t, oldl, oldt); 472 | 473 | if (mOnScrollChangeListener != null) { 474 | mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); 475 | } 476 | } 477 | 478 | @Override 479 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 480 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 481 | 482 | if (!mFillViewport) { 483 | return; 484 | } 485 | 486 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 487 | if (heightMode == MeasureSpec.UNSPECIFIED) { 488 | return; 489 | } 490 | 491 | if (getChildCount() > 0) { 492 | final View child = getChildAt(0); 493 | int height = getMeasuredHeight(); 494 | if (child.getMeasuredHeight() < height) { 495 | final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 496 | 497 | int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 498 | getPaddingLeft() + getPaddingRight(), lp.width); 499 | height -= getPaddingTop(); 500 | height -= getPaddingBottom(); 501 | int childHeightMeasureSpec = 502 | MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 503 | 504 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 505 | } 506 | } 507 | } 508 | 509 | @Override 510 | public boolean dispatchKeyEvent(KeyEvent event) { 511 | // Let the focused view and/or our descendants get the key first 512 | return super.dispatchKeyEvent(event) || executeKeyEvent(event); 513 | } 514 | 515 | /** 516 | * You can call this function yourself to have the scroll view perform 517 | * scrolling from a key event, just as if the event had been dispatched to 518 | * it by the view hierarchy. 519 | * 520 | * @param event The key event to execute. 521 | * @return Return true if the event was handled, else false. 522 | */ 523 | public boolean executeKeyEvent(KeyEvent event) { 524 | mTempRect.setEmpty(); 525 | 526 | if (!canScroll()) { 527 | if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { 528 | View currentFocused = findFocus(); 529 | if (currentFocused == this) currentFocused = null; 530 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, 531 | currentFocused, View.FOCUS_DOWN); 532 | return nextFocused != null 533 | && nextFocused != this 534 | && nextFocused.requestFocus(View.FOCUS_DOWN); 535 | } 536 | return false; 537 | } 538 | 539 | boolean handled = false; 540 | if (event.getAction() == KeyEvent.ACTION_DOWN) { 541 | switch (event.getKeyCode()) { 542 | case KeyEvent.KEYCODE_DPAD_UP: 543 | if (!event.isAltPressed()) { 544 | handled = arrowScroll(View.FOCUS_UP); 545 | } else { 546 | handled = fullScroll(View.FOCUS_UP); 547 | } 548 | break; 549 | case KeyEvent.KEYCODE_DPAD_DOWN: 550 | if (!event.isAltPressed()) { 551 | handled = arrowScroll(View.FOCUS_DOWN); 552 | } else { 553 | handled = fullScroll(View.FOCUS_DOWN); 554 | } 555 | break; 556 | case KeyEvent.KEYCODE_SPACE: 557 | pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); 558 | break; 559 | } 560 | } 561 | 562 | return handled; 563 | } 564 | 565 | private boolean inChild(int x, int y) { 566 | if (getChildCount() > 0) { 567 | final int scrollY = getScrollY(); 568 | final View child = getChildAt(0); 569 | return !(y < child.getTop() - scrollY 570 | || y >= child.getBottom() - scrollY 571 | || x < child.getLeft() 572 | || x >= child.getRight()); 573 | } 574 | return false; 575 | } 576 | 577 | private void initOrResetVelocityTracker() { 578 | if (mVelocityTracker == null) { 579 | mVelocityTracker = VelocityTracker.obtain(); 580 | } else { 581 | mVelocityTracker.clear(); 582 | } 583 | } 584 | 585 | private void initVelocityTrackerIfNotExists() { 586 | if (mVelocityTracker == null) { 587 | mVelocityTracker = VelocityTracker.obtain(); 588 | } 589 | } 590 | 591 | private void recycleVelocityTracker() { 592 | if (mVelocityTracker != null) { 593 | mVelocityTracker.recycle(); 594 | mVelocityTracker = null; 595 | } 596 | } 597 | 598 | @Override 599 | public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 600 | if (disallowIntercept) { 601 | recycleVelocityTracker(); 602 | } 603 | super.requestDisallowInterceptTouchEvent(disallowIntercept); 604 | } 605 | 606 | 607 | @Override 608 | public boolean onInterceptTouchEvent(MotionEvent ev) { 609 | /* 610 | * This method JUST determines whether we want to intercept the motion. 611 | * If we return true, onMotionEvent will be called and we do the actual 612 | * scrolling there. 613 | */ 614 | 615 | /* 616 | * Shortcut the most recurring case: the user is in the dragging 617 | * state and he is moving his finger. We want to intercept this 618 | * motion. 619 | */ 620 | final int action = ev.getAction(); 621 | if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 622 | return true; 623 | } 624 | 625 | switch (action & MotionEventCompat.ACTION_MASK) { 626 | case MotionEvent.ACTION_MOVE: { 627 | /* 628 | * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 629 | * whether the user has moved far enough from his original down touch. 630 | */ 631 | 632 | /* 633 | * Locally do absolute value. mLastMotionY is set to the y value 634 | * of the down event. 635 | */ 636 | final int activePointerId = mActivePointerId; 637 | if (activePointerId == INVALID_POINTER) { 638 | // If we don't have a valid id, the touch down wasn't on content. 639 | break; 640 | } 641 | 642 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 643 | if (pointerIndex == -1) { 644 | Log.e(TAG, "Invalid pointerId=" + activePointerId 645 | + " in onInterceptTouchEvent"); 646 | break; 647 | } 648 | 649 | final int y = (int) MotionEventCompat.getY(ev, pointerIndex); 650 | final int yDiff = Math.abs(y - mLastMotionY); 651 | if (yDiff > mTouchSlop 652 | && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { 653 | mIsBeingDragged = true; 654 | mLastMotionY = y; 655 | initVelocityTrackerIfNotExists(); 656 | mVelocityTracker.addMovement(ev); 657 | mNestedYOffset = 0; 658 | final ViewParent parent = getParent(); 659 | if (parent != null) { 660 | parent.requestDisallowInterceptTouchEvent(true); 661 | } 662 | } 663 | break; 664 | } 665 | 666 | case MotionEvent.ACTION_DOWN: { 667 | final int y = (int) ev.getY(); 668 | if (!inChild((int) ev.getX(), (int) y)) { 669 | mIsBeingDragged = false; 670 | recycleVelocityTracker(); 671 | break; 672 | } 673 | 674 | /* 675 | * Remember location of down touch. 676 | * ACTION_DOWN always refers to pointer index 0. 677 | */ 678 | mLastMotionY = y; 679 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 680 | 681 | initOrResetVelocityTracker(); 682 | mVelocityTracker.addMovement(ev); 683 | /* 684 | * If being flinged and user touches the screen, initiate drag; 685 | * otherwise don't. mScroller.isFinished should be false when 686 | * being flinged. We need to call computeScrollOffset() first so that 687 | * isFinished() is correct. 688 | */ 689 | mScroller.computeScrollOffset(); 690 | mIsBeingDragged = !mScroller.isFinished(); 691 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 692 | break; 693 | } 694 | 695 | case MotionEvent.ACTION_CANCEL: 696 | case MotionEvent.ACTION_UP: 697 | /* Release the drag */ 698 | mIsBeingDragged = false; 699 | mActivePointerId = INVALID_POINTER; 700 | recycleVelocityTracker(); 701 | if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { 702 | ViewCompat.postInvalidateOnAnimation(this); 703 | } 704 | stopNestedScroll(); 705 | break; 706 | case MotionEventCompat.ACTION_POINTER_UP: 707 | onSecondaryPointerUp(ev); 708 | break; 709 | } 710 | 711 | /* 712 | * The only time we want to intercept motion events is if we are in the 713 | * drag mode. 714 | */ 715 | return mIsBeingDragged; 716 | } 717 | 718 | @Override 719 | public boolean onTouchEvent(MotionEvent ev) { 720 | initVelocityTrackerIfNotExists(); 721 | 722 | MotionEvent vtev = MotionEvent.obtain(ev); 723 | 724 | final int actionMasked = MotionEventCompat.getActionMasked(ev); 725 | 726 | if (actionMasked == MotionEvent.ACTION_DOWN) { 727 | mNestedYOffset = 0; 728 | } 729 | vtev.offsetLocation(0, mNestedYOffset); 730 | 731 | switch (actionMasked) { 732 | case MotionEvent.ACTION_DOWN: { 733 | if (getChildCount() == 0) { 734 | return false; 735 | } 736 | if ((mIsBeingDragged = !mScroller.isFinished())) { 737 | final ViewParent parent = getParent(); 738 | if (parent != null) { 739 | parent.requestDisallowInterceptTouchEvent(true); 740 | } 741 | } 742 | 743 | /* 744 | * If being flinged and user touches, stop the fling. isFinished 745 | * will be false if being flinged. 746 | */ 747 | if (!mScroller.isFinished()) { 748 | mScroller.abortAnimation(); 749 | } 750 | 751 | // Remember where the motion event started 752 | mLastMotionY = (int) ev.getY(); 753 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 754 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 755 | break; 756 | } 757 | case MotionEvent.ACTION_MOVE: 758 | final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, 759 | mActivePointerId); 760 | if (activePointerIndex == -1) { 761 | Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 762 | break; 763 | } 764 | 765 | final int y = (int) MotionEventCompat.getY(ev, activePointerIndex); 766 | int deltaY = mLastMotionY - y; 767 | if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { 768 | deltaY -= mScrollConsumed[1]; 769 | vtev.offsetLocation(0, mScrollOffset[1]); 770 | mNestedYOffset += mScrollOffset[1]; 771 | } 772 | if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 773 | final ViewParent parent = getParent(); 774 | if (parent != null) { 775 | parent.requestDisallowInterceptTouchEvent(true); 776 | } 777 | mIsBeingDragged = true; 778 | if (deltaY > 0) { 779 | deltaY -= mTouchSlop; 780 | } else { 781 | deltaY += mTouchSlop; 782 | } 783 | } 784 | if (mIsBeingDragged) { 785 | // Scroll to follow the motion event 786 | mLastMotionY = y - mScrollOffset[1]; 787 | 788 | final int oldY = getScrollY(); 789 | final int range = getScrollRange(); 790 | final int overscrollMode = ViewCompat.getOverScrollMode(this); 791 | boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 792 | (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && 793 | range > 0); 794 | 795 | // Calling overScrollByCompat will call onOverScrolled, which 796 | // calls onScrollChanged if applicable. 797 | if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, 798 | 0, true) && !hasNestedScrollingParent()) { 799 | // Break our velocity if we hit a scroll barrier. 800 | mVelocityTracker.clear(); 801 | } 802 | 803 | final int scrolledDeltaY = getScrollY() - oldY; 804 | final int unconsumedY = deltaY - scrolledDeltaY; 805 | if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { 806 | mLastMotionY -= mScrollOffset[1]; 807 | vtev.offsetLocation(0, mScrollOffset[1]); 808 | mNestedYOffset += mScrollOffset[1]; 809 | } else if (canOverscroll) { 810 | ensureGlows(); 811 | final int pulledToY = oldY + deltaY; 812 | if (pulledToY < 0) { 813 | mEdgeGlowTop.onPull((float) deltaY / getHeight(), 814 | MotionEventCompat.getX(ev, activePointerIndex) / getWidth()); 815 | if (!mEdgeGlowBottom.isFinished()) { 816 | mEdgeGlowBottom.onRelease(); 817 | } 818 | } else if (pulledToY > range) { 819 | mEdgeGlowBottom.onPull((float) deltaY / getHeight(), 820 | 1.f - MotionEventCompat.getX(ev, activePointerIndex) 821 | / getWidth()); 822 | if (!mEdgeGlowTop.isFinished()) { 823 | mEdgeGlowTop.onRelease(); 824 | } 825 | } 826 | if (mEdgeGlowTop != null 827 | && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { 828 | ViewCompat.postInvalidateOnAnimation(this); 829 | } 830 | } 831 | } 832 | break; 833 | case MotionEvent.ACTION_UP: 834 | if (mIsBeingDragged) { 835 | final VelocityTracker velocityTracker = mVelocityTracker; 836 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 837 | int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, 838 | mActivePointerId); 839 | 840 | if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 841 | flingWithNestedDispatch(-initialVelocity); 842 | } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 843 | getScrollRange())) { 844 | ViewCompat.postInvalidateOnAnimation(this); 845 | } 846 | } 847 | mActivePointerId = INVALID_POINTER; 848 | endDrag(); 849 | break; 850 | case MotionEvent.ACTION_CANCEL: 851 | if (mIsBeingDragged && getChildCount() > 0) { 852 | if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 853 | getScrollRange())) { 854 | ViewCompat.postInvalidateOnAnimation(this); 855 | } 856 | } 857 | mActivePointerId = INVALID_POINTER; 858 | endDrag(); 859 | break; 860 | case MotionEventCompat.ACTION_POINTER_DOWN: { 861 | final int index = MotionEventCompat.getActionIndex(ev); 862 | mLastMotionY = (int) MotionEventCompat.getY(ev, index); 863 | mActivePointerId = MotionEventCompat.getPointerId(ev, index); 864 | break; 865 | } 866 | case MotionEventCompat.ACTION_POINTER_UP: 867 | onSecondaryPointerUp(ev); 868 | mLastMotionY = (int) MotionEventCompat.getY(ev, 869 | MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 870 | break; 871 | } 872 | 873 | if (mVelocityTracker != null) { 874 | mVelocityTracker.addMovement(vtev); 875 | } 876 | vtev.recycle(); 877 | return true; 878 | } 879 | 880 | private void onSecondaryPointerUp(MotionEvent ev) { 881 | final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) >> 882 | MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; 883 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 884 | if (pointerId == mActivePointerId) { 885 | // This was our active pointer going up. Choose a new 886 | // active pointer and adjust accordingly. 887 | // TODO: Make this decision more intelligent. 888 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 889 | mLastMotionY = (int) MotionEventCompat.getY(ev, newPointerIndex); 890 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 891 | if (mVelocityTracker != null) { 892 | mVelocityTracker.clear(); 893 | } 894 | } 895 | } 896 | 897 | public boolean onGenericMotionEvent(MotionEvent event) { 898 | if ((MotionEventCompat.getSource(event) & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { 899 | switch (event.getAction()) { 900 | case MotionEventCompat.ACTION_SCROLL: { 901 | if (!mIsBeingDragged) { 902 | final float vscroll = MotionEventCompat.getAxisValue(event, 903 | MotionEventCompat.AXIS_VSCROLL); 904 | if (vscroll != 0) { 905 | final int delta = (int) (vscroll * getVerticalScrollFactorCompat()); 906 | final int range = getScrollRange(); 907 | int oldScrollY = getScrollY(); 908 | int newScrollY = oldScrollY - delta; 909 | if (newScrollY < 0) { 910 | newScrollY = 0; 911 | } else if (newScrollY > range) { 912 | newScrollY = range; 913 | } 914 | if (newScrollY != oldScrollY) { 915 | super.scrollTo(getScrollX(), newScrollY); 916 | return true; 917 | } 918 | } 919 | } 920 | } 921 | } 922 | } 923 | return false; 924 | } 925 | 926 | private float getVerticalScrollFactorCompat() { 927 | if (mVerticalScrollFactor == 0) { 928 | TypedValue outValue = new TypedValue(); 929 | final Context context = getContext(); 930 | if (!context.getTheme().resolveAttribute( 931 | android.R.attr.listPreferredItemHeight, outValue, true)) { 932 | throw new IllegalStateException( 933 | "Expected theme to define listPreferredItemHeight."); 934 | } 935 | mVerticalScrollFactor = outValue.getDimension( 936 | context.getResources().getDisplayMetrics()); 937 | } 938 | return mVerticalScrollFactor; 939 | } 940 | 941 | protected void onOverScrolled(int scrollX, int scrollY, 942 | boolean clampedX, boolean clampedY) { 943 | super.scrollTo(scrollX, scrollY); 944 | } 945 | 946 | boolean overScrollByCompat(int deltaX, int deltaY, 947 | int scrollX, int scrollY, 948 | int scrollRangeX, int scrollRangeY, 949 | int maxOverScrollX, int maxOverScrollY, 950 | boolean isTouchEvent) { 951 | final int overScrollMode = ViewCompat.getOverScrollMode(this); 952 | final boolean canScrollHorizontal = 953 | computeHorizontalScrollRange() > computeHorizontalScrollExtent(); 954 | final boolean canScrollVertical = 955 | computeVerticalScrollRange() > computeVerticalScrollExtent(); 956 | final boolean overScrollHorizontal = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 957 | (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); 958 | final boolean overScrollVertical = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 959 | (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); 960 | 961 | int newScrollX = scrollX + deltaX; 962 | if (!overScrollHorizontal) { 963 | maxOverScrollX = 0; 964 | } 965 | 966 | int newScrollY = scrollY + deltaY; 967 | if (!overScrollVertical) { 968 | maxOverScrollY = 0; 969 | } 970 | 971 | // Clamp values if at the limits and record 972 | final int left = -maxOverScrollX; 973 | final int right = maxOverScrollX + scrollRangeX; 974 | final int top = -maxOverScrollY; 975 | final int bottom = maxOverScrollY + scrollRangeY; 976 | 977 | boolean clampedX = false; 978 | if (newScrollX > right) { 979 | newScrollX = right; 980 | clampedX = true; 981 | } else if (newScrollX < left) { 982 | newScrollX = left; 983 | clampedX = true; 984 | } 985 | 986 | boolean clampedY = false; 987 | if (newScrollY > bottom) { 988 | newScrollY = bottom; 989 | clampedY = true; 990 | } else if (newScrollY < top) { 991 | newScrollY = top; 992 | clampedY = true; 993 | } 994 | 995 | if (clampedY) { 996 | mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); 997 | } 998 | 999 | onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); 1000 | 1001 | return clampedX || clampedY; 1002 | } 1003 | 1004 | private int getScrollRange() { 1005 | int scrollRange = 0; 1006 | if (getChildCount() > 0) { 1007 | View child = getChildAt(0); 1008 | scrollRange = Math.max(0, 1009 | child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); 1010 | } 1011 | return scrollRange; 1012 | } 1013 | 1014 | /** 1015 | *1016 | * Finds the next focusable component that fits in the specified bounds. 1017 | *
1018 | * 1019 | * @param topFocus look for a candidate is the one at the top of the bounds 1020 | * if topFocus is true, or at the bottom of the bounds if topFocus is 1021 | * false 1022 | * @param top the top offset of the bounds in which a focusable must be 1023 | * found 1024 | * @param bottom the bottom offset of the bounds in which a focusable must 1025 | * be found 1026 | * @return the next focusable component in the bounds or null if none can 1027 | * be found 1028 | */ 1029 | private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { 1030 | 1031 | ListHandles scrolling in response to a "page up/down" shortcut press. This 1099 | * method will scroll the view by one page up or down and give the focus 1100 | * to the topmost/bottommost component in the new visible area. If no 1101 | * component is a good candidate for focus, this scrollview reclaims the 1102 | * focus.
1103 | * 1104 | * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1105 | * to go one page up or 1106 | * {@link android.view.View#FOCUS_DOWN} to go one page down 1107 | * @return true if the key event is consumed by this method, false otherwise 1108 | */ 1109 | public boolean pageScroll(int direction) { 1110 | boolean down = direction == View.FOCUS_DOWN; 1111 | int height = getHeight(); 1112 | 1113 | if (down) { 1114 | mTempRect.top = getScrollY() + height; 1115 | int count = getChildCount(); 1116 | if (count > 0) { 1117 | View view = getChildAt(count - 1); 1118 | if (mTempRect.top + height > view.getBottom()) { 1119 | mTempRect.top = view.getBottom() - height; 1120 | } 1121 | } 1122 | } else { 1123 | mTempRect.top = getScrollY() - height; 1124 | if (mTempRect.top < 0) { 1125 | mTempRect.top = 0; 1126 | } 1127 | } 1128 | mTempRect.bottom = mTempRect.top + height; 1129 | 1130 | return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1131 | } 1132 | 1133 | /** 1134 | *Handles scrolling in response to a "home/end" shortcut press. This 1135 | * method will scroll the view to the top or bottom and give the focus 1136 | * to the topmost/bottommost component in the new visible area. If no 1137 | * component is a good candidate for focus, this scrollview reclaims the 1138 | * focus.
1139 | * 1140 | * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1141 | * to go the top of the view or 1142 | * {@link android.view.View#FOCUS_DOWN} to go the bottom 1143 | * @return true if the key event is consumed by this method, false otherwise 1144 | */ 1145 | public boolean fullScroll(int direction) { 1146 | boolean down = direction == View.FOCUS_DOWN; 1147 | int height = getHeight(); 1148 | 1149 | mTempRect.top = 0; 1150 | mTempRect.bottom = height; 1151 | 1152 | if (down) { 1153 | int count = getChildCount(); 1154 | if (count > 0) { 1155 | View view = getChildAt(count - 1); 1156 | mTempRect.bottom = view.getBottom() + getPaddingBottom(); 1157 | mTempRect.top = mTempRect.bottom - height; 1158 | } 1159 | } 1160 | 1161 | return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1162 | } 1163 | 1164 | /** 1165 | *Scrolls the view to make the area defined by top and
1166 | * bottom visible. This method attempts to give the focus
1167 | * to a component visible in this area. If no component can be focused in
1168 | * the new visible area, the focus is reclaimed by this ScrollView.
The scroll range of a scroll view is the overall height of all of its 1336 | * children.
1337 | * @hide 1338 | */ 1339 | @Override 1340 | public int computeVerticalScrollRange() { 1341 | final int count = getChildCount(); 1342 | final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); 1343 | if (count == 0) { 1344 | return contentHeight; 1345 | } 1346 | 1347 | int scrollRange = getChildAt(0).getBottom(); 1348 | final int scrollY = getScrollY(); 1349 | final int overscrollBottom = Math.max(0, scrollRange - contentHeight); 1350 | if (scrollY < 0) { 1351 | scrollRange -= scrollY; 1352 | } else if (scrollY > overscrollBottom) { 1353 | scrollRange += scrollY - overscrollBottom; 1354 | } 1355 | 1356 | return scrollRange; 1357 | } 1358 | 1359 | /** @hide */ 1360 | @Override 1361 | public int computeVerticalScrollOffset() { 1362 | return Math.max(0, super.computeVerticalScrollOffset()); 1363 | } 1364 | 1365 | /** @hide */ 1366 | @Override 1367 | public int computeVerticalScrollExtent() { 1368 | return super.computeVerticalScrollExtent(); 1369 | } 1370 | 1371 | /** @hide */ 1372 | @Override 1373 | public int computeHorizontalScrollRange() { 1374 | return super.computeHorizontalScrollRange(); 1375 | } 1376 | 1377 | /** @hide */ 1378 | @Override 1379 | public int computeHorizontalScrollOffset() { 1380 | return super.computeHorizontalScrollOffset(); 1381 | } 1382 | 1383 | /** @hide */ 1384 | @Override 1385 | public int computeHorizontalScrollExtent() { 1386 | return super.computeHorizontalScrollExtent(); 1387 | } 1388 | 1389 | @Override 1390 | protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { 1391 | ViewGroup.LayoutParams lp = child.getLayoutParams(); 1392 | 1393 | int childWidthMeasureSpec; 1394 | int childHeightMeasureSpec; 1395 | 1396 | childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() 1397 | + getPaddingRight(), lp.width); 1398 | 1399 | childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1400 | 1401 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1402 | } 1403 | 1404 | @Override 1405 | protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 1406 | int parentHeightMeasureSpec, int heightUsed) { 1407 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 1408 | 1409 | final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 1410 | getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin 1411 | + widthUsed, lp.width); 1412 | final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 1413 | lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); 1414 | 1415 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1416 | } 1417 | 1418 | @Override 1419 | public void computeScroll() { 1420 | if (mScroller.computeScrollOffset()) { 1421 | int oldX = getScrollX(); 1422 | int oldY = getScrollY(); 1423 | int x = mScroller.getCurrX(); 1424 | int y = mScroller.getCurrY(); 1425 | 1426 | if (oldX != x || oldY != y) { 1427 | final int range = getScrollRange(); 1428 | final int overscrollMode = ViewCompat.getOverScrollMode(this); 1429 | final boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 1430 | (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 1431 | 1432 | overScrollByCompat(x - oldX, y - oldY, oldX, oldY, 0, range, 1433 | 0, 0, false); 1434 | 1435 | if (canOverscroll) { 1436 | ensureGlows(); 1437 | if (y <= 0 && oldY > 0) { 1438 | mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 1439 | } else if (y >= range && oldY < range) { 1440 | mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); 1441 | } 1442 | } 1443 | } 1444 | } 1445 | } 1446 | 1447 | /** 1448 | * Scrolls the view to the given child. 1449 | * 1450 | * @param child the View to scroll to 1451 | */ 1452 | private void scrollToChild(View child) { 1453 | child.getDrawingRect(mTempRect); 1454 | 1455 | /* Offset from child's local coordinates to ScrollView coordinates */ 1456 | offsetDescendantRectToMyCoords(child, mTempRect); 1457 | 1458 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1459 | 1460 | if (scrollDelta != 0) { 1461 | scrollBy(0, scrollDelta); 1462 | } 1463 | } 1464 | 1465 | /** 1466 | * If rect is off screen, scroll just enough to get it (or at least the 1467 | * first screen size chunk of it) on screen. 1468 | * 1469 | * @param rect The rectangle. 1470 | * @param immediate True to scroll immediately without animation 1471 | * @return true if scrolling was performed 1472 | */ 1473 | private boolean scrollToChildRect(Rect rect, boolean immediate) { 1474 | final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); 1475 | final boolean scroll = delta != 0; 1476 | if (scroll) { 1477 | if (immediate) { 1478 | scrollBy(0, delta); 1479 | } else { 1480 | smoothScrollBy(0, delta); 1481 | } 1482 | } 1483 | return scroll; 1484 | } 1485 | 1486 | /** 1487 | * Compute the amount to scroll in the Y direction in order to get 1488 | * a rectangle completely on the screen (or, if taller than the screen, 1489 | * at least the first screen size chunk of it). 1490 | * 1491 | * @param rect The rect. 1492 | * @return The scroll delta. 1493 | */ 1494 | protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { 1495 | if (getChildCount() == 0) return 0; 1496 | 1497 | int height = getHeight(); 1498 | int screenTop = getScrollY(); 1499 | int screenBottom = screenTop + height; 1500 | 1501 | int fadingEdge = getVerticalFadingEdgeLength(); 1502 | 1503 | // leave room for top fading edge as long as rect isn't at very top 1504 | if (rect.top > 0) { 1505 | screenTop += fadingEdge; 1506 | } 1507 | 1508 | // leave room for bottom fading edge as long as rect isn't at very bottom 1509 | if (rect.bottom < getChildAt(0).getHeight()) { 1510 | screenBottom -= fadingEdge; 1511 | } 1512 | 1513 | int scrollYDelta = 0; 1514 | 1515 | if (rect.bottom > screenBottom && rect.top > screenTop) { 1516 | // need to move down to get it in view: move down just enough so 1517 | // that the entire rectangle is in view (or at least the first 1518 | // screen size chunk). 1519 | 1520 | if (rect.height() > height) { 1521 | // just enough to get screen size chunk on 1522 | scrollYDelta += (rect.top - screenTop); 1523 | } else { 1524 | // get entire rect at bottom of screen 1525 | scrollYDelta += (rect.bottom - screenBottom); 1526 | } 1527 | 1528 | // make sure we aren't scrolling beyond the end of our content 1529 | int bottom = getChildAt(0).getBottom(); 1530 | int distanceToBottom = bottom - screenBottom; 1531 | scrollYDelta = Math.min(scrollYDelta, distanceToBottom); 1532 | 1533 | } else if (rect.top < screenTop && rect.bottom < screenBottom) { 1534 | // need to move up to get it in view: move up just enough so that 1535 | // entire rectangle is in view (or at least the first screen 1536 | // size chunk of it). 1537 | 1538 | if (rect.height() > height) { 1539 | // screen size chunk 1540 | scrollYDelta -= (screenBottom - rect.bottom); 1541 | } else { 1542 | // entire rect at top 1543 | scrollYDelta -= (screenTop - rect.top); 1544 | } 1545 | 1546 | // make sure we aren't scrolling any further than the top our content 1547 | scrollYDelta = Math.max(scrollYDelta, -getScrollY()); 1548 | } 1549 | return scrollYDelta; 1550 | } 1551 | 1552 | @Override 1553 | public void requestChildFocus(View child, View focused) { 1554 | if (!mIsLayoutDirty) { 1555 | scrollToChild(focused); 1556 | } else { 1557 | // The child may not be laid out yet, we can't compute the scroll yet 1558 | mChildToScrollTo = focused; 1559 | } 1560 | super.requestChildFocus(child, focused); 1561 | } 1562 | 1563 | 1564 | /** 1565 | * When looking for focus in children of a scroll view, need to be a little 1566 | * more careful not to give focus to something that is scrolled off screen. 1567 | * 1568 | * This is more expensive than the default {@link android.view.ViewGroup} 1569 | * implementation, otherwise this behavior might have been made the default. 1570 | */ 1571 | @Override 1572 | protected boolean onRequestFocusInDescendants(int direction, 1573 | Rect previouslyFocusedRect) { 1574 | 1575 | // convert from forward / backward notation to up / down / left / right 1576 | // (ugh). 1577 | if (direction == View.FOCUS_FORWARD) { 1578 | direction = View.FOCUS_DOWN; 1579 | } else if (direction == View.FOCUS_BACKWARD) { 1580 | direction = View.FOCUS_UP; 1581 | } 1582 | 1583 | final View nextFocus = previouslyFocusedRect == null ? 1584 | FocusFinder.getInstance().findNextFocus(this, null, direction) : 1585 | FocusFinder.getInstance().findNextFocusFromRect(this, 1586 | previouslyFocusedRect, direction); 1587 | 1588 | if (nextFocus == null) { 1589 | return false; 1590 | } 1591 | 1592 | if (isOffScreen(nextFocus)) { 1593 | return false; 1594 | } 1595 | 1596 | return nextFocus.requestFocus(direction, previouslyFocusedRect); 1597 | } 1598 | 1599 | @Override 1600 | public boolean requestChildRectangleOnScreen(View child, Rect rectangle, 1601 | boolean immediate) { 1602 | // offset into coordinate space of this scroll view 1603 | rectangle.offset(child.getLeft() - child.getScrollX(), 1604 | child.getTop() - child.getScrollY()); 1605 | 1606 | return scrollToChildRect(rectangle, immediate); 1607 | } 1608 | 1609 | @Override 1610 | public void requestLayout() { 1611 | mIsLayoutDirty = true; 1612 | super.requestLayout(); 1613 | } 1614 | 1615 | @Override 1616 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1617 | super.onLayout(changed, l, t, r, b); 1618 | mIsLayoutDirty = false; 1619 | // Give a child focus if it needs it 1620 | if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { 1621 | scrollToChild(mChildToScrollTo); 1622 | } 1623 | mChildToScrollTo = null; 1624 | 1625 | if (!mIsLaidOut) { 1626 | if (mSavedState != null) { 1627 | scrollTo(getScrollX(), mSavedState.scrollPosition); 1628 | mSavedState = null; 1629 | } // mScrollY default value is "0" 1630 | 1631 | final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; 1632 | final int scrollRange = Math.max(0, 1633 | childHeight - (b - t - getPaddingBottom() - getPaddingTop())); 1634 | 1635 | // Don't forget to clamp 1636 | if (getScrollY() > scrollRange) { 1637 | scrollTo(getScrollX(), scrollRange); 1638 | } else if (getScrollY() < 0) { 1639 | scrollTo(getScrollX(), 0); 1640 | } 1641 | } 1642 | 1643 | // Calling this with the present values causes it to re-claim them 1644 | scrollTo(getScrollX(), getScrollY()); 1645 | mIsLaidOut = true; 1646 | } 1647 | 1648 | @Override 1649 | public void onAttachedToWindow() { 1650 | super.onAttachedToWindow(); 1651 | 1652 | mIsLaidOut = false; 1653 | } 1654 | 1655 | @Override 1656 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1657 | super.onSizeChanged(w, h, oldw, oldh); 1658 | 1659 | View currentFocused = findFocus(); 1660 | if (null == currentFocused || this == currentFocused) 1661 | return; 1662 | 1663 | // If the currently-focused view was visible on the screen when the 1664 | // screen was at the old height, then scroll the screen to make that 1665 | // view visible with the new screen height. 1666 | if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { 1667 | currentFocused.getDrawingRect(mTempRect); 1668 | offsetDescendantRectToMyCoords(currentFocused, mTempRect); 1669 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1670 | doScrollY(scrollDelta); 1671 | } 1672 | } 1673 | 1674 | /** 1675 | * Return true if child is a descendant of parent, (or equal to the parent). 1676 | */ 1677 | private static boolean isViewDescendantOf(View child, View parent) { 1678 | if (child == parent) { 1679 | return true; 1680 | } 1681 | 1682 | final ViewParent theParent = child.getParent(); 1683 | return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); 1684 | } 1685 | 1686 | /** 1687 | * Fling the scroll view 1688 | * 1689 | * @param velocityY The initial velocity in the Y direction. Positive 1690 | * numbers mean that the finger/cursor is moving down the screen, 1691 | * which means we want to scroll towards the top. 1692 | */ 1693 | public void fling(int velocityY) { 1694 | if (getChildCount() > 0) { 1695 | int height = getHeight() - getPaddingBottom() - getPaddingTop(); 1696 | int bottom = getChildAt(0).getHeight(); 1697 | 1698 | mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, 1699 | Math.max(0, bottom - height), 0, height/2); 1700 | 1701 | ViewCompat.postInvalidateOnAnimation(this); 1702 | } 1703 | } 1704 | 1705 | private void flingWithNestedDispatch(int velocityY) { 1706 | final int scrollY = getScrollY(); 1707 | final boolean canFling = (scrollY > 0 || velocityY > 0) && 1708 | (scrollY < getScrollRange() || velocityY < 0); 1709 | if (!dispatchNestedPreFling(0, velocityY)) { 1710 | dispatchNestedFling(0, velocityY, canFling); 1711 | if (canFling) { 1712 | fling(velocityY); 1713 | } 1714 | } 1715 | } 1716 | 1717 | private void endDrag() { 1718 | mIsBeingDragged = false; 1719 | 1720 | recycleVelocityTracker(); 1721 | stopNestedScroll(); 1722 | 1723 | if (mEdgeGlowTop != null) { 1724 | mEdgeGlowTop.onRelease(); 1725 | mEdgeGlowBottom.onRelease(); 1726 | } 1727 | } 1728 | 1729 | /** 1730 | * {@inheritDoc} 1731 | * 1732 | *This version also clamps the scrolling to the bounds of our child.
1733 | */
1734 | @Override
1735 | public void scrollTo(int x, int y) {
1736 | // we rely on the fact the View.scrollBy calls scrollTo.
1737 | if (getChildCount() > 0) {
1738 | View child = getChildAt(0);
1739 | x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
1740 | y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
1741 | if (x != getScrollX() || y != getScrollY()) {
1742 | super.scrollTo(x, y);
1743 | }
1744 | }
1745 | }
1746 |
1747 | private void ensureGlows() {
1748 | if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
1749 | if (mEdgeGlowTop == null) {
1750 | Context context = getContext();
1751 | mEdgeGlowTop = new EdgeEffectCompat(context);
1752 | mEdgeGlowBottom = new EdgeEffectCompat(context);
1753 | }
1754 | } else {
1755 | mEdgeGlowTop = null;
1756 | mEdgeGlowBottom = null;
1757 | }
1758 | }
1759 |
1760 | @Override
1761 | public void draw(Canvas canvas) {
1762 | super.draw(canvas);
1763 | if (mEdgeGlowTop != null) {
1764 | final int scrollY = getScrollY();
1765 | if (!mEdgeGlowTop.isFinished()) {
1766 | final int restoreCount = canvas.save();
1767 | final int width = getWidth() - getPaddingLeft() - getPaddingRight();
1768 |
1769 | canvas.translate(getPaddingLeft(), Math.min(0, scrollY));
1770 | mEdgeGlowTop.setSize(width, getHeight());
1771 | if (mEdgeGlowTop.draw(canvas)) {
1772 | ViewCompat.postInvalidateOnAnimation(this);
1773 | }
1774 | canvas.restoreToCount(restoreCount);
1775 | }
1776 | if (!mEdgeGlowBottom.isFinished()) {
1777 | final int restoreCount = canvas.save();
1778 | final int width = getWidth() - getPaddingLeft() - getPaddingRight();
1779 | final int height = getHeight();
1780 |
1781 | canvas.translate(-width + getPaddingLeft(),
1782 | Math.max(getScrollRange(), scrollY) + height);
1783 | canvas.rotate(180, width, 0);
1784 | mEdgeGlowBottom.setSize(width, height);
1785 | if (mEdgeGlowBottom.draw(canvas)) {
1786 | ViewCompat.postInvalidateOnAnimation(this);
1787 | }
1788 | canvas.restoreToCount(restoreCount);
1789 | }
1790 | }
1791 | }
1792 |
1793 | private static int clamp(int n, int my, int child) {
1794 | if (my >= child || n < 0) {
1795 | /* my >= child is this case:
1796 | * |--------------- me ---------------|
1797 | * |------ child ------|
1798 | * or
1799 | * |--------------- me ---------------|
1800 | * |------ child ------|
1801 | * or
1802 | * |--------------- me ---------------|
1803 | * |------ child ------|
1804 | *
1805 | * n < 0 is this case:
1806 | * |------ me ------|
1807 | * |-------- child --------|
1808 | * |-- mScrollX --|
1809 | */
1810 | return 0;
1811 | }
1812 | if ((my+n) > child) {
1813 | /* this case:
1814 | * |------ me ------|
1815 | * |------ child ------|
1816 | * |-- mScrollX --|
1817 | */
1818 | return child-my;
1819 | }
1820 | return n;
1821 | }
1822 |
1823 | @Override
1824 | protected void onRestoreInstanceState(Parcelable state) {
1825 | if (!(state instanceof SavedState)) {
1826 | super.onRestoreInstanceState(state);
1827 | return;
1828 | }
1829 |
1830 | SavedState ss = (SavedState) state;
1831 | super.onRestoreInstanceState(ss.getSuperState());
1832 | mSavedState = ss;
1833 | requestLayout();
1834 | }
1835 |
1836 | @Override
1837 | protected Parcelable onSaveInstanceState() {
1838 | Parcelable superState = super.onSaveInstanceState();
1839 | SavedState ss = new SavedState(superState);
1840 | ss.scrollPosition = getScrollY();
1841 | return ss;
1842 | }
1843 |
1844 | static class SavedState extends BaseSavedState {
1845 | public int scrollPosition;
1846 |
1847 | SavedState(Parcelable superState) {
1848 | super(superState);
1849 | }
1850 |
1851 | public SavedState(Parcel source) {
1852 | super(source);
1853 | scrollPosition = source.readInt();
1854 | }
1855 |
1856 | @Override
1857 | public void writeToParcel(Parcel dest, int flags) {
1858 | super.writeToParcel(dest, flags);
1859 | dest.writeInt(scrollPosition);
1860 | }
1861 |
1862 | @Override
1863 | public String toString() {
1864 | return "HorizontalScrollView.SavedState{"
1865 | + Integer.toHexString(System.identityHashCode(this))
1866 | + " scrollPosition=" + scrollPosition + "}";
1867 | }
1868 |
1869 | public static final Parcelable.Creator