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