5 |
6 | ```java
7 | allprojects {
8 | repositories {
9 | ...
10 | maven { url 'https://www.jitpack.io' }
11 | }
12 | }
13 | dependencies {
14 | implementation 'com.github.Tijn1314:NestedScrollWebView:1.1'
15 | }
16 | ```
17 | ## Usage
18 | In your layout.xml include the following:
19 | ```java
20 | 32 | * WebView compatible with CoordinatorLayout. 33 | * The implementation based on NestedScrollView of design library 34 | */ 35 | public class NestedScrollWebView extends WebView implements NestedScrollingChild2, NestedScrollingParent { 36 | 37 | private static final int INVALID_POINTER = -1; 38 | private static final String TAG = "NestedWebView"; 39 | 40 | private final int[] mScrollOffset = new int[2]; 41 | private final int[] mScrollConsumed = new int[2]; 42 | 43 | private int mLastMotionY; 44 | private NestedScrollingParentHelper mParentHelper; 45 | private NestedScrollingChildHelper mChildHelper; 46 | private boolean mIsBeingDragged = false; 47 | private VelocityTracker mVelocityTracker; 48 | private int mTouchSlop; 49 | private int mActivePointerId = INVALID_POINTER; 50 | private int mNestedYOffset; 51 | private OverScroller mScroller; 52 | private int mMinimumVelocity; 53 | private int mMaximumVelocity; 54 | private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); 55 | 56 | public NestedScrollWebView(Context context) { 57 | this(context, null); 58 | } 59 | 60 | public NestedScrollWebView(Context context, AttributeSet attrs) { 61 | this(context, attrs, android.R.attr.webViewStyle); 62 | } 63 | 64 | public NestedScrollWebView(Context context, AttributeSet attrs, int defStyleAttr) { 65 | super(context, attrs, defStyleAttr); 66 | setOverScrollMode(WebView.OVER_SCROLL_NEVER); 67 | initScrollView(); 68 | mChildHelper = new NestedScrollingChildHelper(this); 69 | mParentHelper = new NestedScrollingParentHelper(this); 70 | setNestedScrollingEnabled(true); 71 | ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); 72 | } 73 | 74 | private void initScrollView() { 75 | mScroller = new OverScroller(getContext()); 76 | final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 77 | mTouchSlop = configuration.getScaledTouchSlop(); 78 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 79 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 80 | } 81 | 82 | @Override 83 | public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 84 | if (disallowIntercept) { 85 | recycleVelocityTracker(); 86 | } 87 | super.requestDisallowInterceptTouchEvent(disallowIntercept); 88 | } 89 | 90 | @Override 91 | public boolean onInterceptTouchEvent(MotionEvent ev) { 92 | 93 | final int action = ev.getAction(); 94 | if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 95 | return true; 96 | } 97 | 98 | switch (action & MotionEvent.ACTION_MASK) { 99 | case MotionEvent.ACTION_MOVE: { 100 | final int activePointerId = mActivePointerId; 101 | if (activePointerId == INVALID_POINTER) { 102 | break; 103 | } 104 | 105 | final int pointerIndex = ev.findPointerIndex(activePointerId); 106 | if (pointerIndex == -1) { 107 | Log.e(TAG, "Invalid pointerId=" + activePointerId 108 | + " in onInterceptTouchEvent"); 109 | break; 110 | } 111 | 112 | final int y = (int) ev.getY(pointerIndex); 113 | final int yDiff = Math.abs(y - mLastMotionY); 114 | if (yDiff > mTouchSlop 115 | && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { 116 | mIsBeingDragged = true; 117 | mLastMotionY = y; 118 | initVelocityTrackerIfNotExists(); 119 | mVelocityTracker.addMovement(ev); 120 | mNestedYOffset = 0; 121 | final ViewParent parent = getParent(); 122 | if (parent != null) { 123 | parent.requestDisallowInterceptTouchEvent(true); 124 | } 125 | } 126 | break; 127 | } 128 | 129 | case MotionEvent.ACTION_DOWN: { 130 | final int y = (int) ev.getY(); 131 | 132 | mLastMotionY = y; 133 | mActivePointerId = ev.getPointerId(0); 134 | 135 | initOrResetVelocityTracker(); 136 | mVelocityTracker.addMovement(ev); 137 | 138 | mScroller.computeScrollOffset(); 139 | mIsBeingDragged = !mScroller.isFinished(); 140 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 141 | break; 142 | } 143 | 144 | case MotionEvent.ACTION_CANCEL: 145 | case MotionEvent.ACTION_UP: 146 | mIsBeingDragged = false; 147 | mActivePointerId = INVALID_POINTER; 148 | recycleVelocityTracker(); 149 | if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { 150 | ViewCompat.postInvalidateOnAnimation(this); 151 | } 152 | stopNestedScroll(); 153 | break; 154 | case MotionEvent.ACTION_POINTER_UP: 155 | onSecondaryPointerUp(ev); 156 | break; 157 | } 158 | 159 | return mIsBeingDragged; 160 | } 161 | 162 | 163 | @Override 164 | public boolean onTouchEvent(MotionEvent ev) { 165 | boolean returnValue = false; 166 | initVelocityTrackerIfNotExists(); 167 | 168 | MotionEvent vtev = MotionEvent.obtain(ev); 169 | 170 | final int actionMasked = ev.getActionMasked(); 171 | 172 | if (actionMasked == MotionEvent.ACTION_DOWN) { 173 | mNestedYOffset = 0; 174 | } 175 | vtev.offsetLocation(0, mNestedYOffset); 176 | 177 | switch (actionMasked) { 178 | case MotionEvent.ACTION_DOWN: { 179 | returnValue = super.onTouchEvent(ev); 180 | if (mIsBeingDragged = !mScroller.isFinished()) { 181 | final ViewParent parent = getParent(); 182 | if (parent != null) { 183 | parent.requestDisallowInterceptTouchEvent(true); 184 | } 185 | } 186 | 187 | if (!mScroller.isFinished()) { 188 | mScroller.abortAnimation(); 189 | } 190 | mLastMotionY = (int) ev.getY(); 191 | mActivePointerId = ev.getPointerId(0); 192 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); 193 | break; 194 | } 195 | case MotionEvent.ACTION_MOVE: 196 | final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 197 | if (activePointerIndex == -1) { 198 | Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 199 | break; 200 | } 201 | 202 | final int y = (int) ev.getY(activePointerIndex); 203 | int deltaY = mLastMotionY - y; 204 | if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, 205 | ViewCompat.TYPE_TOUCH)) { 206 | deltaY -= mScrollConsumed[1]; 207 | ev.offsetLocation(0, -mScrollOffset[1]); 208 | vtev.offsetLocation(0, -mScrollOffset[1]); 209 | mNestedYOffset += mScrollOffset[1]; 210 | } 211 | 212 | boolean notMove = mScrollOffset[1] == 0; 213 | if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 214 | final ViewParent parent = getParent(); 215 | if (parent != null) { 216 | parent.requestDisallowInterceptTouchEvent(true); 217 | } 218 | mIsBeingDragged = true; 219 | if (deltaY > 0) { 220 | deltaY -= mTouchSlop; 221 | } else { 222 | deltaY += mTouchSlop; 223 | } 224 | } 225 | if (mIsBeingDragged) { 226 | mLastMotionY = y - mScrollOffset[1]; 227 | // final int oldY = getScrollY(); 228 | // final int range = getScrollRange(); 229 | // int unconsumedY = 0; 230 | // int scrolledDeltaY = deltaY; 231 | // int expectScroll = oldY + deltaY; 232 | // if (expectScroll < 0) { 233 | // unconsumedY = expectScroll; 234 | // scrolledDeltaY = oldY; 235 | // } else if (expectScroll > range) { 236 | // unconsumedY = range - expectScroll; 237 | // scrolledDeltaY = expectScroll - range; 238 | // } 239 | //借鉴tobiasrohloff的滚动处理 处理抖动问题 240 | final int oldY = getScrollY(); 241 | int newScrollY = Math.max(0, oldY + deltaY); 242 | int scrolledDeltaY = newScrollY - oldY; 243 | int unconsumedY = deltaY - scrolledDeltaY; 244 | if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, 245 | ViewCompat.TYPE_TOUCH)) { 246 | mLastMotionY -= mScrollOffset[1]; 247 | vtev.offsetLocation(0, mScrollOffset[1]); 248 | mNestedYOffset += mScrollOffset[1]; 249 | } 250 | } 251 | notMove &= (mScrollOffset[1] == 0); 252 | if (notMove) { 253 | returnValue = super.onTouchEvent(ev); 254 | } else { 255 | final ViewParent parent = getParent(); 256 | if (parent != null) { 257 | parent.requestDisallowInterceptTouchEvent(true); 258 | } 259 | } 260 | break; 261 | case MotionEvent.ACTION_UP: 262 | if (Math.abs(mNestedYOffset) < mTouchSlop) { 263 | returnValue = super.onTouchEvent(ev); 264 | } else { 265 | ev.setAction(MotionEvent.ACTION_CANCEL); 266 | returnValue = super.onTouchEvent(ev); 267 | } 268 | final VelocityTracker velocityTracker = mVelocityTracker; 269 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 270 | int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 271 | if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 272 | flingWithNestedDispatch(-initialVelocity); 273 | } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 274 | getScrollRange())) { 275 | ViewCompat.postInvalidateOnAnimation(this); 276 | } 277 | mActivePointerId = INVALID_POINTER; 278 | endDrag(); 279 | break; 280 | case MotionEvent.ACTION_CANCEL: 281 | returnValue = true; 282 | if (mIsBeingDragged && getChildCount() > 0) { 283 | if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 284 | getScrollRange())) { 285 | ViewCompat.postInvalidateOnAnimation(this); 286 | } 287 | } 288 | mActivePointerId = INVALID_POINTER; 289 | endDrag(); 290 | break; 291 | case MotionEvent.ACTION_POINTER_DOWN: { 292 | final int index = ev.getActionIndex(); 293 | mLastMotionY = (int) ev.getY(index); 294 | mActivePointerId = ev.getPointerId(index); 295 | break; 296 | } 297 | case MotionEvent.ACTION_POINTER_UP: 298 | onSecondaryPointerUp(ev); 299 | mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 300 | break; 301 | } 302 | 303 | if (mVelocityTracker != null) { 304 | mVelocityTracker.addMovement(vtev); 305 | } 306 | vtev.recycle(); 307 | return returnValue; 308 | } 309 | 310 | 311 | int getScrollRange() { 312 | //Using scroll range of webview instead of childs as NestedScrollView does. 313 | return computeVerticalScrollRange(); 314 | } 315 | 316 | @Override 317 | protected void onOverScrolled(int scrollX, int scrollY, 318 | boolean clampedX, boolean clampedY) { 319 | super.scrollTo(scrollX, scrollY); 320 | } 321 | 322 | boolean overScrollByCompat(int deltaX, int deltaY, 323 | int scrollX, int scrollY, 324 | int scrollRangeX, int scrollRangeY, 325 | int maxOverScrollX, int maxOverScrollY, 326 | boolean isTouchEvent) { 327 | final int overScrollMode = getOverScrollMode(); 328 | final boolean canScrollHorizontal = 329 | computeHorizontalScrollRange() > computeHorizontalScrollExtent(); 330 | final boolean canScrollVertical = 331 | computeVerticalScrollRange() > computeVerticalScrollExtent(); 332 | final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS 333 | || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); 334 | final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS 335 | || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); 336 | 337 | int newScrollX = scrollX + deltaX; 338 | if (!overScrollHorizontal) { 339 | maxOverScrollX = 0; 340 | } 341 | 342 | int newScrollY = scrollY + deltaY; 343 | if (!overScrollVertical) { 344 | maxOverScrollY = 0; 345 | } 346 | 347 | // Clamp values if at the limits and record 348 | final int left = -maxOverScrollX; 349 | final int right = maxOverScrollX + scrollRangeX; 350 | final int top = -maxOverScrollY; 351 | final int bottom = maxOverScrollY + scrollRangeY; 352 | 353 | boolean clampedX = false; 354 | if (newScrollX > right) { 355 | newScrollX = right; 356 | clampedX = true; 357 | } else if (newScrollX < left) { 358 | newScrollX = left; 359 | clampedX = true; 360 | } 361 | 362 | boolean clampedY = false; 363 | if (newScrollY > bottom) { 364 | newScrollY = bottom; 365 | clampedY = true; 366 | } else if (newScrollY < top) { 367 | newScrollY = top; 368 | clampedY = true; 369 | } 370 | 371 | if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { 372 | mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); 373 | } 374 | 375 | onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); 376 | 377 | return clampedX || clampedY; 378 | } 379 | 380 | private float mVerticalScrollFactor; 381 | 382 | @Override 383 | public boolean onGenericMotionEvent(MotionEvent event) { 384 | if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { 385 | switch (event.getAction()) { 386 | case MotionEvent.ACTION_SCROLL: { 387 | if (!mIsBeingDragged) { 388 | final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 389 | if (vscroll != 0) { 390 | final int delta = (int) (vscroll * getVerticalScrollFactorCompat()); 391 | final int range = getScrollRange(); 392 | int oldScrollY = getScrollY(); 393 | int newScrollY = oldScrollY - delta; 394 | if (newScrollY < 0) { 395 | newScrollY = 0; 396 | } else if (newScrollY > range) { 397 | newScrollY = range; 398 | } 399 | if (newScrollY != oldScrollY) { 400 | super.scrollTo(getScrollX(), newScrollY); 401 | return true; 402 | } 403 | } 404 | } 405 | } 406 | } 407 | } 408 | return false; 409 | } 410 | 411 | private float getVerticalScrollFactorCompat() { 412 | if (mVerticalScrollFactor == 0) { 413 | TypedValue outValue = new TypedValue(); 414 | final Context context = getContext(); 415 | if (!context.getTheme().resolveAttribute( 416 | android.R.attr.listPreferredItemHeight, outValue, true)) { 417 | throw new IllegalStateException( 418 | "Expected theme to define listPreferredItemHeight."); 419 | } 420 | mVerticalScrollFactor = outValue.getDimension( 421 | context.getResources().getDisplayMetrics()); 422 | } 423 | return mVerticalScrollFactor; 424 | } 425 | 426 | 427 | private void endDrag() { 428 | mIsBeingDragged = false; 429 | recycleVelocityTracker(); 430 | stopNestedScroll(); 431 | } 432 | 433 | private void onSecondaryPointerUp(MotionEvent ev) { 434 | final int pointerIndex = ev.getActionIndex(); 435 | final int pointerId = ev.getPointerId(pointerIndex); 436 | if (pointerId == mActivePointerId) { 437 | // This was our active pointer going up. Choose a new 438 | // active pointer and adjust accordingly. 439 | // TODO: Make this decision more intelligent. 440 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 441 | mLastMotionY = (int) ev.getY(newPointerIndex); 442 | mActivePointerId = ev.getPointerId(newPointerIndex); 443 | if (mVelocityTracker != null) { 444 | mVelocityTracker.clear(); 445 | } 446 | } 447 | } 448 | 449 | private void initOrResetVelocityTracker() { 450 | if (mVelocityTracker == null) { 451 | mVelocityTracker = VelocityTracker.obtain(); 452 | } else { 453 | mVelocityTracker.clear(); 454 | } 455 | } 456 | 457 | private void initVelocityTrackerIfNotExists() { 458 | if (mVelocityTracker == null) { 459 | mVelocityTracker = VelocityTracker.obtain(); 460 | } 461 | } 462 | 463 | private void recycleVelocityTracker() { 464 | if (mVelocityTracker != null) { 465 | mVelocityTracker.recycle(); 466 | mVelocityTracker = null; 467 | } 468 | } 469 | 470 | private void flingWithNestedDispatch(int velocityY) { 471 | final int scrollY = getScrollY(); 472 | final boolean canFling = (scrollY > 0 || velocityY > 0) 473 | && (scrollY < getScrollRange() || velocityY < 0); 474 | if (!dispatchNestedPreFling(0, velocityY)) { 475 | dispatchNestedFling(0, velocityY, canFling); 476 | fling(velocityY); 477 | } 478 | } 479 | 480 | public void fling(int velocityY) { 481 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH); 482 | mScroller.fling(getScrollX(), getScrollY(), // start 483 | 0, velocityY, // velocities 484 | 0, 0, // x 485 | Integer.MIN_VALUE, Integer.MAX_VALUE, // y 486 | 0, 0); // overscroll 487 | mLastScrollerY = getScrollY(); 488 | ViewCompat.postInvalidateOnAnimation(this); 489 | } 490 | 491 | private int mLastScrollerY; 492 | 493 | @Override 494 | public void computeScroll() { 495 | if (mScroller.computeScrollOffset()) { 496 | final int x = mScroller.getCurrX(); 497 | final int y = mScroller.getCurrY(); 498 | 499 | int dy = y - mLastScrollerY; 500 | 501 | // Dispatch up to parent 502 | if (dispatchNestedPreScroll(0, dy, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)) { 503 | dy -= mScrollConsumed[1]; 504 | } 505 | 506 | if (dy != 0) { 507 | final int range = getScrollRange(); 508 | final int oldScrollY = getScrollY(); 509 | 510 | overScrollByCompat(0, dy, getScrollX(), oldScrollY, 0, range, 0, 0, false); 511 | 512 | final int scrolledDeltaY = getScrollY() - oldScrollY; 513 | final int unconsumedY = dy - scrolledDeltaY; 514 | 515 | if (!dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, null, 516 | ViewCompat.TYPE_NON_TOUCH)) { 517 | final int mode = getOverScrollMode(); 518 | final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS 519 | || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 520 | if (canOverscroll) { 521 | // ensureGlows(); 522 | // if (y <= 0 && oldScrollY > 0) { 523 | // mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 524 | // } else if (y >= range && oldScrollY < range) { 525 | // mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); 526 | // } 527 | } 528 | } 529 | } 530 | 531 | // Finally update the scroll positions and post an invalidation 532 | mLastScrollerY = y; 533 | ViewCompat.postInvalidateOnAnimation(this); 534 | } else { 535 | // We can't scroll any more, so stop any indirect scrolling 536 | if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { 537 | stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); 538 | } 539 | // and reset the scroller y 540 | mLastScrollerY = 0; 541 | } 542 | } 543 | 544 | @Override 545 | public boolean isNestedScrollingEnabled() { 546 | return mChildHelper.isNestedScrollingEnabled(); 547 | } 548 | 549 | @Override 550 | public void setNestedScrollingEnabled(boolean enabled) { 551 | mChildHelper.setNestedScrollingEnabled(enabled); 552 | } 553 | 554 | @Override 555 | public boolean startNestedScroll(int axes) { 556 | return mChildHelper.startNestedScroll(axes); 557 | } 558 | 559 | @Override 560 | public void stopNestedScroll() { 561 | mChildHelper.stopNestedScroll(); 562 | } 563 | 564 | @Override 565 | public boolean hasNestedScrollingParent() { 566 | return mChildHelper.hasNestedScrollingParent(); 567 | } 568 | 569 | @Override 570 | public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, 571 | int[] offsetInWindow) { 572 | return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); 573 | } 574 | 575 | @Override 576 | public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { 577 | return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); 578 | } 579 | 580 | @Override 581 | public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { 582 | return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); 583 | } 584 | 585 | @Override 586 | public boolean dispatchNestedPreFling(float velocityX, float velocityY) { 587 | return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); 588 | } 589 | 590 | @Override 591 | public int getNestedScrollAxes() { 592 | return mParentHelper.getNestedScrollAxes(); 593 | } 594 | 595 | @Override 596 | public boolean startNestedScroll(int axes, int type) { 597 | return mChildHelper.startNestedScroll(axes, type); 598 | } 599 | 600 | @Override 601 | public void stopNestedScroll(int type) { 602 | mChildHelper.stopNestedScroll(type); 603 | } 604 | 605 | @Override 606 | public boolean hasNestedScrollingParent(int type) { 607 | return mChildHelper.hasNestedScrollingParent(type); 608 | } 609 | 610 | @Override 611 | public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, 612 | int dyUnconsumed, int[] offsetInWindow, int type) { 613 | return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, 614 | offsetInWindow, type); 615 | } 616 | 617 | @Override 618 | public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, 619 | int type) { 620 | return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); 621 | } 622 | 623 | @Override 624 | public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, 625 | int dyUnconsumed) { 626 | final int oldScrollY = getScrollY(); 627 | scrollBy(0, dyUnconsumed); 628 | final int myConsumed = getScrollY() - oldScrollY; 629 | final int myUnconsumed = dyUnconsumed - myConsumed; 630 | dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); 631 | } 632 | 633 | @Override 634 | public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 635 | dispatchNestedPreScroll(dx, dy, consumed, null); 636 | } 637 | 638 | @Override 639 | public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 640 | if (!consumed) { 641 | flingWithNestedDispatch((int) velocityY); 642 | return true; 643 | } 644 | return false; 645 | } 646 | 647 | @Override 648 | public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 649 | return dispatchNestedPreFling(velocityX, velocityY); 650 | } 651 | 652 | // nested scroll parent 653 | @Override 654 | public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 655 | return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; 656 | } 657 | 658 | @Override 659 | public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { 660 | mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); 661 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 662 | } 663 | 664 | @Override 665 | public void onStopNestedScroll(View target) { 666 | mParentHelper.onStopNestedScroll(target); 667 | stopNestedScroll(); 668 | } 669 | 670 | /** 671 | * Like {@link #scrollTo}, but scroll smoothly instead of immediately. 672 | * 673 | * @param x the position where to scroll on the X axis 674 | * @param y the position where to scroll on the Y axis 675 | */ 676 | public final void smoothScrollTo(int x, int y) { 677 | smoothScrollBy(x - getScrollX(), y - getScrollY()); 678 | } 679 | 680 | /** 681 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 682 | * 683 | * @param dx the number of pixels to scroll by on the X axis 684 | * @param dy the number of pixels to scroll by on the Y axis 685 | */ 686 | private long mLastScroll; 687 | static final int ANIMATED_SCROLL_GAP = 250; 688 | 689 | public final void smoothScrollBy(int dx, int dy) { 690 | long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; 691 | if (duration > ANIMATED_SCROLL_GAP) { 692 | final int height = getHeight() - getPaddingBottom() - getPaddingTop(); 693 | final int bottom = getHeight(); 694 | final int maxY = Math.max(0, bottom - height); 695 | final int scrollY = getScrollY(); 696 | dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; 697 | 698 | mScroller.startScroll(getScrollX(), scrollY, 0, dy); 699 | ViewCompat.postInvalidateOnAnimation(this); 700 | } else { 701 | if (!mScroller.isFinished()) { 702 | mScroller.abortAnimation(); 703 | } 704 | scrollBy(dx, dy); 705 | } 706 | mLastScroll = AnimationUtils.currentAnimationTimeMillis(); 707 | } 708 | 709 | static class AccessibilityDelegate extends AccessibilityDelegateCompat { 710 | @Override 711 | public boolean performAccessibilityAction(View host, int action, Bundle arguments) { 712 | if (super.performAccessibilityAction(host, action, arguments)) { 713 | return true; 714 | } 715 | final NestedScrollWebView nsvHost = (NestedScrollWebView) host; 716 | if (!nsvHost.isEnabled()) { 717 | return false; 718 | } 719 | switch (action) { 720 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { 721 | final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() 722 | - nsvHost.getPaddingTop(); 723 | final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight, 724 | nsvHost.getScrollRange()); 725 | if (targetScrollY != nsvHost.getScrollY()) { 726 | nsvHost.smoothScrollTo(0, targetScrollY); 727 | return true; 728 | } 729 | } 730 | return false; 731 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { 732 | final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() 733 | - nsvHost.getPaddingTop(); 734 | final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0); 735 | if (targetScrollY != nsvHost.getScrollY()) { 736 | nsvHost.smoothScrollTo(0, targetScrollY); 737 | return true; 738 | } 739 | } 740 | return false; 741 | } 742 | return false; 743 | } 744 | 745 | @Override 746 | public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 747 | super.onInitializeAccessibilityNodeInfo(host, info); 748 | final NestedScrollWebView nsvHost = (NestedScrollWebView) host; 749 | info.setClassName(ScrollView.class.getName()); 750 | if (nsvHost.isEnabled()) { 751 | final int scrollRange = nsvHost.getScrollRange(); 752 | if (scrollRange > 0) { 753 | info.setScrollable(true); 754 | if (nsvHost.getScrollY() > 0) { 755 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 756 | } 757 | if (nsvHost.getScrollY() < scrollRange) { 758 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 759 | } 760 | } 761 | } 762 | } 763 | 764 | @Override 765 | public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 766 | super.onInitializeAccessibilityEvent(host, event); 767 | final NestedScrollWebView nsvHost = (NestedScrollWebView) host; 768 | event.setClassName(ScrollView.class.getName()); 769 | final boolean scrollable = nsvHost.getScrollRange() > 0; 770 | event.setScrollable(scrollable); 771 | event.setScrollX(nsvHost.getScrollX()); 772 | event.setScrollY(nsvHost.getScrollY()); 773 | AccessibilityRecordCompat.setMaxScrollX(event, nsvHost.getScrollX()); 774 | AccessibilityRecordCompat.setMaxScrollY(event, nsvHost.getScrollRange()); 775 | } 776 | } 777 | 778 | /** 779 | * Stop any current scroll 780 | * #fling(int, int)} or a touch-initiated fling. 781 | */ 782 | public void stopScroll() { 783 | if (mScroller != null) { 784 | mScroller.forceFinished(true); 785 | } 786 | } 787 | } 788 | --------------------------------------------------------------------------------