1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 18 package androidx.core.widget; 19 20 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 21 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.graphics.Canvas; 25 import android.graphics.Rect; 26 import android.os.Build; 27 import android.os.Bundle; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.util.TypedValue; 33 import android.view.FocusFinder; 34 import android.view.KeyEvent; 35 import android.view.MotionEvent; 36 import android.view.VelocityTracker; 37 import android.view.View; 38 import android.view.ViewConfiguration; 39 import android.view.ViewGroup; 40 import android.view.ViewParent; 41 import android.view.accessibility.AccessibilityEvent; 42 import android.view.animation.AnimationUtils; 43 import android.widget.EdgeEffect; 44 import android.widget.FrameLayout; 45 import android.widget.OverScroller; 46 import android.widget.ScrollView; 47 48 import androidx.annotation.NonNull; 49 import androidx.annotation.Nullable; 50 import androidx.annotation.RestrictTo; 51 import androidx.core.view.AccessibilityDelegateCompat; 52 import androidx.core.view.InputDeviceCompat; 53 import androidx.core.view.NestedScrollingChild2; 54 import androidx.core.view.NestedScrollingChildHelper; 55 import androidx.core.view.NestedScrollingParent2; 56 import androidx.core.view.NestedScrollingParentHelper; 57 import androidx.core.view.ScrollingView; 58 import androidx.core.view.ViewCompat; 59 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 60 import androidx.core.view.accessibility.AccessibilityRecordCompat; 61 62 import java.util.List; 63 64 /** 65 * NestedScrollView is just like {@link android.widget.ScrollView}, but it supports acting 66 * as both a nested scrolling parent and child on both new and old versions of Android. 67 * Nested scrolling is enabled by default. 68 */ 69 public class NestedScrollView extends FrameLayout implements NestedScrollingParent2, 70 NestedScrollingChild2, ScrollingView { 71 static final int ANIMATED_SCROLL_GAP = 250; 72 73 static final float MAX_SCROLL_FACTOR = 0.5f; 74 75 private static final String TAG = "NestedScrollView"; 76 77 /** 78 * Interface definition for a callback to be invoked when the scroll 79 * X or Y positions of a view change. 80 * 81 * <p>This version of the interface works on all versions of Android, back to API v4.</p> 82 * 83 * @see #setOnScrollChangeListener(OnScrollChangeListener) 84 */ 85 public interface OnScrollChangeListener { 86 /** 87 * Called when the scroll position of a view changes. 88 * 89 * @param v The view whose scroll position has changed. 90 * @param scrollX Current horizontal scroll origin. 91 * @param scrollY Current vertical scroll origin. 92 * @param oldScrollX Previous horizontal scroll origin. 93 * @param oldScrollY Previous vertical scroll origin. 94 */ 95 void onScrollChange(NestedScrollView v, int scrollX, int scrollY, 96 int oldScrollX, int oldScrollY); 97 } 98 99 private long mLastScroll; 100 101 private final Rect mTempRect = new Rect(); 102 private OverScroller mScroller; 103 private EdgeEffect mEdgeGlowTop; 104 private EdgeEffect mEdgeGlowBottom; 105 106 /** 107 * Position of the last motion event. 108 */ 109 private int mLastMotionY; 110 111 /** 112 * True when the layout has changed but the traversal has not come through yet. 113 * Ideally the view hierarchy would keep track of this for us. 114 */ 115 private boolean mIsLayoutDirty = true; 116 private boolean mIsLaidOut = false; 117 118 /** 119 * The child to give focus to in the event that a child has requested focus while the 120 * layout is dirty. This prevents the scroll from being wrong if the child has not been 121 * laid out before requesting focus. 122 */ 123 private View mChildToScrollTo = null; 124 125 /** 126 * True if the user is currently dragging this ScrollView around. This is 127 * not the same as 'is being flinged', which can be checked by 128 * mScroller.isFinished() (flinging begins when the user lifts his finger). 129 */ 130 private boolean mIsBeingDragged = false; 131 132 /** 133 * Determines speed during touch scrolling 134 */ 135 private VelocityTracker mVelocityTracker; 136 137 /** 138 * When set to true, the scroll view measure its child to make it fill the currently 139 * visible area. 140 */ 141 private boolean mFillViewport; 142 143 /** 144 * Whether arrow scrolling is animated. 145 */ 146 private boolean mSmoothScrollingEnabled = true; 147 148 private int mTouchSlop; 149 private int mMinimumVelocity; 150 private int mMaximumVelocity; 151 152 /** 153 * ID of the active pointer. This is used to retain consistency during 154 * drags/flings if multiple pointers are used. 155 */ 156 private int mActivePointerId = INVALID_POINTER; 157 158 /** 159 * Used during scrolling to retrieve the new offset within the window. 160 */ 161 private final int[] mScrollOffset = new int[2]; 162 private final int[] mScrollConsumed = new int[2]; 163 private int mNestedYOffset; 164 165 private int mLastScrollerY; 166 167 /** 168 * Sentinel value for no current active pointer. 169 * Used by {@link #mActivePointerId}. 170 */ 171 private static final int INVALID_POINTER = -1; 172 173 private SavedState mSavedState; 174 175 private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); 176 177 private static final int[] SCROLLVIEW_STYLEABLE = new int[] { 178 android.R.attr.fillViewport 179 }; 180 181 private final NestedScrollingParentHelper mParentHelper; 182 private final NestedScrollingChildHelper mChildHelper; 183 184 private float mVerticalScrollFactor; 185 186 private OnScrollChangeListener mOnScrollChangeListener; 187 188 public NestedScrollView(@NonNull Context context) { 189 this(context, null); 190 } 191 192 public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) { 193 this(context, attrs, 0); 194 } 195 196 public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs, 197 int defStyleAttr) { 198 super(context, attrs, defStyleAttr); 199 initScrollView(); 200 201 final TypedArray a = context.obtainStyledAttributes( 202 attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); 203 204 setFillViewport(a.getBoolean(0, false)); 205 206 a.recycle(); 207 208 mParentHelper = new NestedScrollingParentHelper(this); 209 mChildHelper = new NestedScrollingChildHelper(this); 210 211 // ...because why else would you be using this widget? 212 setNestedScrollingEnabled(true); 213 214 ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); 215 } 216 217 // NestedScrollingChild2 218 219 @Override 220 public boolean startNestedScroll(int axes, int type) { 221 return mChildHelper.startNestedScroll(axes, type); 222 } 223 224 @Override 225 public void stopNestedScroll(int type) { 226 mChildHelper.stopNestedScroll(type); 227 } 228 229 @Override 230 public boolean hasNestedScrollingParent(int type) { 231 return mChildHelper.hasNestedScrollingParent(type); 232 } 233 234 @Override 235 public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, 236 int dyUnconsumed, int[] offsetInWindow, int type) { 237 return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, 238 offsetInWindow, type); 239 } 240 241 @Override 242 public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, 243 int type) { 244 return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); 245 } 246 247 // NestedScrollingChild 248 249 @Override 250 public void setNestedScrollingEnabled(boolean enabled) { 251 mChildHelper.setNestedScrollingEnabled(enabled); 252 } 253 254 @Override 255 public boolean isNestedScrollingEnabled() { 256 return mChildHelper.isNestedScrollingEnabled(); 257 } 258 259 @Override 260 public boolean startNestedScroll(int axes) { 261 return startNestedScroll(axes, ViewCompat.TYPE_TOUCH); 262 } 263 264 @Override 265 public void stopNestedScroll() { 266 stopNestedScroll(ViewCompat.TYPE_TOUCH); 267 } 268 269 @Override 270 public boolean hasNestedScrollingParent() { 271 return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH); 272 } 273 274 @Override 275 public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, 276 int dyUnconsumed, int[] offsetInWindow) { 277 return dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, 278 offsetInWindow, ViewCompat.TYPE_TOUCH); 279 } 280 281 @Override 282 public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { 283 return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH); 284 } 285 286 @Override 287 public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { 288 return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); 289 } 290 291 @Override 292 public boolean dispatchNestedPreFling(float velocityX, float velocityY) { 293 return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); 294 } 295 296 // NestedScrollingParent2 297 298 @Override 299 public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, 300 int type) { 301 return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; 302 } 303 304 @Override 305 public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, 306 int type) { 307 mParentHelper.onNestedScrollAccepted(child, target, axes, type); 308 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type); 309 } 310 311 @Override 312 public void onStopNestedScroll(@NonNull View target, int type) { 313 mParentHelper.onStopNestedScroll(target, type); 314 stopNestedScroll(type); 315 } 316 317 @Override 318 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, 319 int dyUnconsumed, int type) { 320 final int oldScrollY = getScrollY(); 321 scrollBy(0, dyUnconsumed); 322 final int myConsumed = getScrollY() - oldScrollY; 323 final int myUnconsumed = dyUnconsumed - myConsumed; 324 dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, 325 type); 326 } 327 328 @Override 329 public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, 330 int type) { 331 dispatchNestedPreScroll(dx, dy, consumed, null, type); 332 } 333 334 // NestedScrollingParent 335 336 @Override 337 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 338 return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH); 339 } 340 341 @Override 342 public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { 343 onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH); 344 } 345 346 @Override 347 public void onStopNestedScroll(View target) { 348 onStopNestedScroll(target, ViewCompat.TYPE_TOUCH); 349 } 350 351 @Override 352 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, 353 int dyUnconsumed) { 354 onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, 355 ViewCompat.TYPE_TOUCH); 356 } 357 358 @Override 359 public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 360 onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH); 361 } 362 363 @Override 364 public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 365 if (!consumed) { 366 flingWithNestedDispatch((int) velocityY); 367 return true; 368 } 369 return false; 370 } 371 372 @Override 373 public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 374 return dispatchNestedPreFling(velocityX, velocityY); 375 } 376 377 @Override 378 public int getNestedScrollAxes() { 379 return mParentHelper.getNestedScrollAxes(); 380 } 381 382 // ScrollView import 383 384 @Override 385 public boolean shouldDelayChildPressedState() { 386 return true; 387 } 388 389 @Override 390 protected float getTopFadingEdgeStrength() { 391 if (getChildCount() == 0) { 392 return 0.0f; 393 } 394 395 final int length = getVerticalFadingEdgeLength(); 396 final int scrollY = getScrollY(); 397 if (scrollY < length) { 398 return scrollY / (float) length; 399 } 400 401 return 1.0f; 402 } 403 404 @Override 405 protected float getBottomFadingEdgeStrength() { 406 if (getChildCount() == 0) { 407 return 0.0f; 408 } 409 410 View child = getChildAt(0); 411 final NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 412 final int length = getVerticalFadingEdgeLength(); 413 final int bottomEdge = getHeight() - getPaddingBottom(); 414 final int span = child.getBottom() + lp.bottomMargin - getScrollY() - bottomEdge; 415 if (span < length) { 416 return span / (float) length; 417 } 418 419 return 1.0f; 420 } 421 422 /** 423 * @return The maximum amount this scroll view will scroll in response to 424 * an arrow event. 425 */ 426 public int getMaxScrollAmount() { 427 return (int) (MAX_SCROLL_FACTOR * getHeight()); 428 } 429 430 private void initScrollView() { 431 mScroller = new OverScroller(getContext()); 432 setFocusable(true); 433 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 434 setWillNotDraw(false); 435 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 436 mTouchSlop = configuration.getScaledTouchSlop(); 437 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 438 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 439 } 440 441 @Override 442 public void addView(View child) { 443 if (getChildCount() > 0) { 444 throw new IllegalStateException("ScrollView can host only one direct child"); 445 } 446 447 super.addView(child); 448 } 449 450 @Override 451 public void addView(View child, int index) { 452 if (getChildCount() > 0) { 453 throw new IllegalStateException("ScrollView can host only one direct child"); 454 } 455 456 super.addView(child, index); 457 } 458 459 @Override 460 public void addView(View child, ViewGroup.LayoutParams params) { 461 if (getChildCount() > 0) { 462 throw new IllegalStateException("ScrollView can host only one direct child"); 463 } 464 465 super.addView(child, params); 466 } 467 468 @Override 469 public void addView(View child, int index, ViewGroup.LayoutParams params) { 470 if (getChildCount() > 0) { 471 throw new IllegalStateException("ScrollView can host only one direct child"); 472 } 473 474 super.addView(child, index, params); 475 } 476 477 /** 478 * Register a callback to be invoked when the scroll X or Y positions of 479 * this view change. 480 * <p>This version of the method works on all versions of Android, back to API v4.</p> 481 * 482 * @param l The listener to notify when the scroll X or Y position changes. 483 * @see android.view.View#getScrollX() 484 * @see android.view.View#getScrollY() 485 */ 486 public void setOnScrollChangeListener(@Nullable OnScrollChangeListener l) { 487 mOnScrollChangeListener = l; 488 } 489 490 /** 491 * @return Returns true this ScrollView can be scrolled 492 */ 493 private boolean canScroll() { 494 if (getChildCount() > 0) { 495 View child = getChildAt(0); 496 final NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 497 int childSize = child.getHeight() + lp.topMargin + lp.bottomMargin; 498 int parentSpace = getHeight() - getPaddingTop() - getPaddingBottom(); 499 return childSize > parentSpace; 500 } 501 return false; 502 } 503 504 /** 505 * Indicates whether this ScrollView's content is stretched to fill the viewport. 506 * 507 * @return True if the content fills the viewport, false otherwise. 508 * 509 * @attr name android:fillViewport 510 */ 511 public boolean isFillViewport() { 512 return mFillViewport; 513 } 514 515 /** 516 * Set whether this ScrollView should stretch its content height to fill the viewport or not. 517 * 518 * @param fillViewport True to stretch the content's height to the viewport's 519 * boundaries, false otherwise. 520 * 521 * @attr name android:fillViewport 522 */ 523 public void setFillViewport(boolean fillViewport) { 524 if (fillViewport != mFillViewport) { 525 mFillViewport = fillViewport; 526 requestLayout(); 527 } 528 } 529 530 /** 531 * @return Whether arrow scrolling will animate its transition. 532 */ 533 public boolean isSmoothScrollingEnabled() { 534 return mSmoothScrollingEnabled; 535 } 536 537 /** 538 * Set whether arrow scrolling will animate its transition. 539 * @param smoothScrollingEnabled whether arrow scrolling will animate its transition 540 */ 541 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { 542 mSmoothScrollingEnabled = smoothScrollingEnabled; 543 } 544 545 @Override 546 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 547 super.onScrollChanged(l, t, oldl, oldt); 548 549 if (mOnScrollChangeListener != null) { 550 mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); 551 } 552 } 553 554 @Override 555 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 556 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 557 558 if (!mFillViewport) { 559 return; 560 } 561 562 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 563 if (heightMode == MeasureSpec.UNSPECIFIED) { 564 return; 565 } 566 567 if (getChildCount() > 0) { 568 View child = getChildAt(0); 569 final NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 570 571 int childSize = child.getMeasuredHeight(); 572 int parentSpace = getMeasuredHeight() 573 - getPaddingTop() 574 - getPaddingBottom() 575 - lp.topMargin 576 - lp.bottomMargin; 577 578 if (childSize < parentSpace) { 579 int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 580 getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, 581 lp.width); 582 int childHeightMeasureSpec = 583 MeasureSpec.makeMeasureSpec(parentSpace, MeasureSpec.EXACTLY); 584 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 585 } 586 } 587 } 588 589 @Override 590 public boolean dispatchKeyEvent(KeyEvent event) { 591 // Let the focused view and/or our descendants get the key first 592 return super.dispatchKeyEvent(event) || executeKeyEvent(event); 593 } 594 595 /** 596 * You can call this function yourself to have the scroll view perform 597 * scrolling from a key event, just as if the event had been dispatched to 598 * it by the view hierarchy. 599 * 600 * @param event The key event to execute. 601 * @return Return true if the event was handled, else false. 602 */ 603 public boolean executeKeyEvent(@NonNull KeyEvent event) { 604 mTempRect.setEmpty(); 605 606 if (!canScroll()) { 607 if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { 608 View currentFocused = findFocus(); 609 if (currentFocused == this) currentFocused = null; 610 View nextFocused = FocusFinder.getInstance().findNextFocus(this, 611 currentFocused, View.FOCUS_DOWN); 612 return nextFocused != null 613 && nextFocused != this 614 && nextFocused.requestFocus(View.FOCUS_DOWN); 615 } 616 return false; 617 } 618 619 boolean handled = false; 620 if (event.getAction() == KeyEvent.ACTION_DOWN) { 621 switch (event.getKeyCode()) { 622 case KeyEvent.KEYCODE_DPAD_UP: 623 if (!event.isAltPressed()) { 624 handled = arrowScroll(View.FOCUS_UP); 625 } else { 626 handled = fullScroll(View.FOCUS_UP); 627 } 628 break; 629 case KeyEvent.KEYCODE_DPAD_DOWN: 630 if (!event.isAltPressed()) { 631 handled = arrowScroll(View.FOCUS_DOWN); 632 } else { 633 handled = fullScroll(View.FOCUS_DOWN); 634 } 635 break; 636 case KeyEvent.KEYCODE_SPACE: 637 pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); 638 break; 639 } 640 } 641 642 return handled; 643 } 644 645 private boolean inChild(int x, int y) { 646 if (getChildCount() > 0) { 647 final int scrollY = getScrollY(); 648 final View child = getChildAt(0); 649 return !(y < child.getTop() - scrollY 650 || y >= child.getBottom() - scrollY 651 || x < child.getLeft() 652 || x >= child.getRight()); 653 } 654 return false; 655 } 656 657 private void initOrResetVelocityTracker() { 658 if (mVelocityTracker == null) { 659 mVelocityTracker = VelocityTracker.obtain(); 660 } else { 661 mVelocityTracker.clear(); 662 } 663 } 664 665 private void initVelocityTrackerIfNotExists() { 666 if (mVelocityTracker == null) { 667 mVelocityTracker = VelocityTracker.obtain(); 668 } 669 } 670 671 private void recycleVelocityTracker() { 672 if (mVelocityTracker != null) { 673 mVelocityTracker.recycle(); 674 mVelocityTracker = null; 675 } 676 } 677 678 @Override 679 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 680 if (disallowIntercept) { 681 recycleVelocityTracker(); 682 } 683 super.requestDisallowInterceptTouchEvent(disallowIntercept); 684 } 685 686 @Override 687 public boolean onInterceptTouchEvent(MotionEvent ev) { 688 /* 689 * This method JUST determines whether we want to intercept the motion. 690 * If we return true, onMotionEvent will be called and we do the actual 691 * scrolling there. 692 */ 693 694 /* 695 * Shortcut the most recurring case: the user is in the dragging 696 * state and he is moving his finger. We want to intercept this 697 * motion. 698 */ 699 final int action = ev.getAction(); 700 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 701 return true; 702 } 703 704 switch (action & MotionEvent.ACTION_MASK) { 705 case MotionEvent.ACTION_MOVE: { 706 /* 707 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 708 * whether the user has moved far enough from his original down touch. 709 */ 710 711 /* 712 * Locally do absolute value. mLastMotionY is set to the y value 713 * of the down event. 714 */ 715 final int activePointerId = mActivePointerId; 716 if (activePointerId == INVALID_POINTER) { 717 // If we don't have a valid id, the touch down wasn't on content. 718 break; 719 } 720 721 final int pointerIndex = ev.findPointerIndex(activePointerId); 722 if (pointerIndex == -1) { 723 Log.e(TAG, "Invalid pointerId=" + activePointerId 724 + " in onInterceptTouchEvent"); 725 break; 726 } 727 728 final int y = (int) ev.getY(pointerIndex); 729 final int yDiff = Math.abs(y - mLastMotionY); 730 if (yDiff > mTouchSlop 731 && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { 732 mIsBeingDragged = true; 733 mLastMotionY = y; 734 initVelocityTrackerIfNotExists(); 735 mVelocityTracker.addMovement(ev); 736 mNestedYOffset = 0; 737 final ViewParent parent = getParent(); 738 if (parent != null) { 739 parent.requestDisallowInterceptTouchEvent(true); 740 } 741 } 742 break; 743 } 744 745 case MotionEvent.ACTION_DOWN: { 746 final int y = (int) ev.getY(); 747 if (!inChild((int) ev.getX(), y)) { 748 mIsBeingDragged = false; 749 recycleVelocityTracker(); 750 break; 751 } 752 753 /* 754 * Remember location of down touch. 755 * ACTION_DOWN always refers to pointer index 0. 756 */ 757 mLastMotionY = y; 758 mActivePointerId = ev.getPointerId(0); 759 760 initOrResetVelocityTracker(); 761 mVelocityTracker.addMovement(ev); 762 /* 763 * If being flinged and user touches the screen, initiate drag; 764 * otherwise don't. mScroller.isFinished should be false when 765 * being flinged. We need to call computeScrollOffset() first so that 766 * isFinished() is correct. 767 */ 768 mScroller.computeScrollOffset(); 769 mIsBeingDragged = !mScroller.isFinished(); 770 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); 771 break; 772 } 773 774 case MotionEvent.ACTION_CANCEL: 775 case MotionEvent.ACTION_UP: 776 /* Release the drag */ 777 mIsBeingDragged = false; 778 mActivePointerId = INVALID_POINTER; 779 recycleVelocityTracker(); 780 if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { 781 ViewCompat.postInvalidateOnAnimation(this); 782 } 783 stopNestedScroll(ViewCompat.TYPE_TOUCH); 784 break; 785 case MotionEvent.ACTION_POINTER_UP: 786 onSecondaryPointerUp(ev); 787 break; 788 } 789 790 /* 791 * The only time we want to intercept motion events is if we are in the 792 * drag mode. 793 */ 794 return mIsBeingDragged; 795 } 796 797 @Override 798 public boolean onTouchEvent(MotionEvent ev) { 799 initVelocityTrackerIfNotExists(); 800 801 MotionEvent vtev = MotionEvent.obtain(ev); 802 803 final int actionMasked = ev.getActionMasked(); 804 805 if (actionMasked == MotionEvent.ACTION_DOWN) { 806 mNestedYOffset = 0; 807 } 808 vtev.offsetLocation(0, mNestedYOffset); 809 810 switch (actionMasked) { 811 case MotionEvent.ACTION_DOWN: { 812 if (getChildCount() == 0) { 813 return false; 814 } 815 if ((mIsBeingDragged = !mScroller.isFinished())) { 816 final ViewParent parent = getParent(); 817 if (parent != null) { 818 parent.requestDisallowInterceptTouchEvent(true); 819 } 820 } 821 822 /* 823 * If being flinged and user touches, stop the fling. isFinished 824 * will be false if being flinged. 825 */ 826 if (!mScroller.isFinished()) { 827 mScroller.abortAnimation(); 828 } 829 830 // Remember where the motion event started 831 mLastMotionY = (int) ev.getY(); 832 mActivePointerId = ev.getPointerId(0); 833 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); 834 break; 835 } 836 case MotionEvent.ACTION_MOVE: 837 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 838 if (activePointerIndex == -1) { 839 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 840 break; 841 } 842 843 final int y = (int) ev.getY(activePointerIndex); 844 int deltaY = mLastMotionY - y; 845 if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, 846 ViewCompat.TYPE_TOUCH)) { 847 deltaY -= mScrollConsumed[1]; 848 vtev.offsetLocation(0, mScrollOffset[1]); 849 mNestedYOffset += mScrollOffset[1]; 850 } 851 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 852 final ViewParent parent = getParent(); 853 if (parent != null) { 854 parent.requestDisallowInterceptTouchEvent(true); 855 } 856 mIsBeingDragged = true; 857 if (deltaY > 0) { 858 deltaY -= mTouchSlop; 859 } else { 860 deltaY += mTouchSlop; 861 } 862 } 863 if (mIsBeingDragged) { 864 // Scroll to follow the motion event 865 mLastMotionY = y - mScrollOffset[1]; 866 867 final int oldY = getScrollY(); 868 final int range = getScrollRange(); 869 final int overscrollMode = getOverScrollMode(); 870 boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS 871 || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 872 873 // Calling overScrollByCompat will call onOverScrolled, which 874 // calls onScrollChanged if applicable. 875 if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, 876 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) { 877 // Break our velocity if we hit a scroll barrier. 878 mVelocityTracker.clear(); 879 } 880 881 final int scrolledDeltaY = getScrollY() - oldY; 882 final int unconsumedY = deltaY - scrolledDeltaY; 883 if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, 884 ViewCompat.TYPE_TOUCH)) { 885 mLastMotionY -= mScrollOffset[1]; 886 vtev.offsetLocation(0, mScrollOffset[1]); 887 mNestedYOffset += mScrollOffset[1]; 888 } else if (canOverscroll) { 889 ensureGlows(); 890 final int pulledToY = oldY + deltaY; 891 if (pulledToY < 0) { 892 EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(), 893 ev.getX(activePointerIndex) / getWidth()); 894 if (!mEdgeGlowBottom.isFinished()) { 895 mEdgeGlowBottom.onRelease(); 896 } 897 } else if (pulledToY > range) { 898 EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(), 899 1.f - ev.getX(activePointerIndex) 900 / getWidth()); 901 if (!mEdgeGlowTop.isFinished()) { 902 mEdgeGlowTop.onRelease(); 903 } 904 } 905 if (mEdgeGlowTop != null 906 && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { 907 ViewCompat.postInvalidateOnAnimation(this); 908 } 909 } 910 } 911 break; 912 case MotionEvent.ACTION_UP: 913 final VelocityTracker velocityTracker = mVelocityTracker; 914 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 915 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 916 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 917 flingWithNestedDispatch(-initialVelocity); 918 } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 919 getScrollRange())) { 920 ViewCompat.postInvalidateOnAnimation(this); 921 } 922 mActivePointerId = INVALID_POINTER; 923 endDrag(); 924 break; 925 case MotionEvent.ACTION_CANCEL: 926 if (mIsBeingDragged && getChildCount() > 0) { 927 if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 928 getScrollRange())) { 929 ViewCompat.postInvalidateOnAnimation(this); 930 } 931 } 932 mActivePointerId = INVALID_POINTER; 933 endDrag(); 934 break; 935 case MotionEvent.ACTION_POINTER_DOWN: { 936 final int index = ev.getActionIndex(); 937 mLastMotionY = (int) ev.getY(index); 938 mActivePointerId = ev.getPointerId(index); 939 break; 940 } 941 case MotionEvent.ACTION_POINTER_UP: 942 onSecondaryPointerUp(ev); 943 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 944 break; 945 } 946 947 if (mVelocityTracker != null) { 948 mVelocityTracker.addMovement(vtev); 949 } 950 vtev.recycle(); 951 return true; 952 } 953 954 private void onSecondaryPointerUp(MotionEvent ev) { 955 final int pointerIndex = ev.getActionIndex(); 956 final int pointerId = ev.getPointerId(pointerIndex); 957 if (pointerId == mActivePointerId) { 958 // This was our active pointer going up. Choose a new 959 // active pointer and adjust accordingly. 960 // TODO: Make this decision more intelligent. 961 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 962 mLastMotionY = (int) ev.getY(newPointerIndex); 963 mActivePointerId = ev.getPointerId(newPointerIndex); 964 if (mVelocityTracker != null) { 965 mVelocityTracker.clear(); 966 } 967 } 968 } 969 970 @Override 971 public boolean onGenericMotionEvent(MotionEvent event) { 972 if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { 973 switch (event.getAction()) { 974 case MotionEvent.ACTION_SCROLL: { 975 if (!mIsBeingDragged) { 976 final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 977 if (vscroll != 0) { 978 final int delta = (int) (vscroll * getVerticalScrollFactorCompat()); 979 final int range = getScrollRange(); 980 int oldScrollY = getScrollY(); 981 int newScrollY = oldScrollY - delta; 982 if (newScrollY < 0) { 983 newScrollY = 0; 984 } else if (newScrollY > range) { 985 newScrollY = range; 986 } 987 if (newScrollY != oldScrollY) { 988 super.scrollTo(getScrollX(), newScrollY); 989 return true; 990 } 991 } 992 } 993 } 994 } 995 } 996 return false; 997 } 998 999 private float getVerticalScrollFactorCompat() { 1000 if (mVerticalScrollFactor == 0) { 1001 TypedValue outValue = new TypedValue(); 1002 final Context context = getContext(); 1003 if (!context.getTheme().resolveAttribute( 1004 android.R.attr.listPreferredItemHeight, outValue, true)) { 1005 throw new IllegalStateException( 1006 "Expected theme to define listPreferredItemHeight."); 1007 } 1008 mVerticalScrollFactor = outValue.getDimension( 1009 context.getResources().getDisplayMetrics()); 1010 } 1011 return mVerticalScrollFactor; 1012 } 1013 1014 @Override 1015 protected void onOverScrolled(int scrollX, int scrollY, 1016 boolean clampedX, boolean clampedY) { 1017 super.scrollTo(scrollX, scrollY); 1018 } 1019 1020 boolean overScrollByCompat(int deltaX, int deltaY, 1021 int scrollX, int scrollY, 1022 int scrollRangeX, int scrollRangeY, 1023 int maxOverScrollX, int maxOverScrollY, 1024 boolean isTouchEvent) { 1025 final int overScrollMode = getOverScrollMode(); 1026 final boolean canScrollHorizontal = 1027 computeHorizontalScrollRange() > computeHorizontalScrollExtent(); 1028 final boolean canScrollVertical = 1029 computeVerticalScrollRange() > computeVerticalScrollExtent(); 1030 final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS 1031 || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); 1032 final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS 1033 || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); 1034 1035 int newScrollX = scrollX + deltaX; 1036 if (!overScrollHorizontal) { 1037 maxOverScrollX = 0; 1038 } 1039 1040 int newScrollY = scrollY + deltaY; 1041 if (!overScrollVertical) { 1042 maxOverScrollY = 0; 1043 } 1044 1045 // Clamp values if at the limits and record 1046 final int left = -maxOverScrollX; 1047 final int right = maxOverScrollX + scrollRangeX; 1048 final int top = -maxOverScrollY; 1049 final int bottom = maxOverScrollY + scrollRangeY; 1050 1051 boolean clampedX = false; 1052 if (newScrollX > right) { 1053 newScrollX = right; 1054 clampedX = true; 1055 } else if (newScrollX < left) { 1056 newScrollX = left; 1057 clampedX = true; 1058 } 1059 1060 boolean clampedY = false; 1061 if (newScrollY > bottom) { 1062 newScrollY = bottom; 1063 clampedY = true; 1064 } else if (newScrollY < top) { 1065 newScrollY = top; 1066 clampedY = true; 1067 } 1068 1069 if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { 1070 mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); 1071 } 1072 1073 onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); 1074 1075 return clampedX || clampedY; 1076 } 1077 1078 int getScrollRange() { 1079 int scrollRange = 0; 1080 if (getChildCount() > 0) { 1081 View child = getChildAt(0); 1082 NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1083 int childSize = child.getHeight() + lp.topMargin + lp.bottomMargin; 1084 int parentSpace = getHeight() - getPaddingTop() - getPaddingBottom(); 1085 scrollRange = Math.max(0, childSize - parentSpace); 1086 } 1087 return scrollRange; 1088 } 1089 1090 /** 1091 * <p> 1092 * Finds the next focusable component that fits in the specified bounds. 1093 * </p> 1094 * 1095 * @param topFocus look for a candidate is the one at the top of the bounds 1096 * if topFocus is true, or at the bottom of the bounds if topFocus is 1097 * false 1098 * @param top the top offset of the bounds in which a focusable must be 1099 * found 1100 * @param bottom the bottom offset of the bounds in which a focusable must 1101 * be found 1102 * @return the next focusable component in the bounds or null if none can 1103 * be found 1104 */ 1105 private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { 1106 1107 List<View> focusables = getFocusables(View.FOCUS_FORWARD); 1108 View focusCandidate = null; 1109 1110 /* 1111 * A fully contained focusable is one where its top is below the bound's 1112 * top, and its bottom is above the bound's bottom. A partially 1113 * contained focusable is one where some part of it is within the 1114 * bounds, but it also has some part that is not within bounds. A fully contained 1115 * focusable is preferred to a partially contained focusable. 1116 */ 1117 boolean foundFullyContainedFocusable = false; 1118 1119 int count = focusables.size(); 1120 for (int i = 0; i < count; i++) { 1121 View view = focusables.get(i); 1122 int viewTop = view.getTop(); 1123 int viewBottom = view.getBottom(); 1124 1125 if (top < viewBottom && viewTop < bottom) { 1126 /* 1127 * the focusable is in the target area, it is a candidate for 1128 * focusing 1129 */ 1130 1131 final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom); 1132 1133 if (focusCandidate == null) { 1134 /* No candidate, take this one */ 1135 focusCandidate = view; 1136 foundFullyContainedFocusable = viewIsFullyContained; 1137 } else { 1138 final boolean viewIsCloserToBoundary = 1139 (topFocus && viewTop < focusCandidate.getTop()) 1140 || (!topFocus && viewBottom > focusCandidate.getBottom()); 1141 1142 if (foundFullyContainedFocusable) { 1143 if (viewIsFullyContained && viewIsCloserToBoundary) { 1144 /* 1145 * We're dealing with only fully contained views, so 1146 * it has to be closer to the boundary to beat our 1147 * candidate 1148 */ 1149 focusCandidate = view; 1150 } 1151 } else { 1152 if (viewIsFullyContained) { 1153 /* Any fully contained view beats a partially contained view */ 1154 focusCandidate = view; 1155 foundFullyContainedFocusable = true; 1156 } else if (viewIsCloserToBoundary) { 1157 /* 1158 * Partially contained view beats another partially 1159 * contained view if it's closer 1160 */ 1161 focusCandidate = view; 1162 } 1163 } 1164 } 1165 } 1166 } 1167 1168 return focusCandidate; 1169 } 1170 1171 /** 1172 * <p>Handles scrolling in response to a "page up/down" shortcut press. This 1173 * method will scroll the view by one page up or down and give the focus 1174 * to the topmost/bottommost component in the new visible area. If no 1175 * component is a good candidate for focus, this scrollview reclaims the 1176 * focus.</p> 1177 * 1178 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1179 * to go one page up or 1180 * {@link android.view.View#FOCUS_DOWN} to go one page down 1181 * @return true if the key event is consumed by this method, false otherwise 1182 */ 1183 public boolean pageScroll(int direction) { 1184 boolean down = direction == View.FOCUS_DOWN; 1185 int height = getHeight(); 1186 1187 if (down) { 1188 mTempRect.top = getScrollY() + height; 1189 int count = getChildCount(); 1190 if (count > 0) { 1191 View view = getChildAt(count - 1); 1192 NestedScrollView.LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1193 int bottom = view.getBottom() + lp.bottomMargin + getPaddingBottom(); 1194 if (mTempRect.top + height > bottom) { 1195 mTempRect.top = bottom - height; 1196 } 1197 } 1198 } else { 1199 mTempRect.top = getScrollY() - height; 1200 if (mTempRect.top < 0) { 1201 mTempRect.top = 0; 1202 } 1203 } 1204 mTempRect.bottom = mTempRect.top + height; 1205 1206 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1207 } 1208 1209 /** 1210 * <p>Handles scrolling in response to a "home/end" shortcut press. This 1211 * method will scroll the view to the top or bottom and give the focus 1212 * to the topmost/bottommost component in the new visible area. If no 1213 * component is a good candidate for focus, this scrollview reclaims the 1214 * focus.</p> 1215 * 1216 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1217 * to go the top of the view or 1218 * {@link android.view.View#FOCUS_DOWN} to go the bottom 1219 * @return true if the key event is consumed by this method, false otherwise 1220 */ 1221 public boolean fullScroll(int direction) { 1222 boolean down = direction == View.FOCUS_DOWN; 1223 int height = getHeight(); 1224 1225 mTempRect.top = 0; 1226 mTempRect.bottom = height; 1227 1228 if (down) { 1229 int count = getChildCount(); 1230 if (count > 0) { 1231 View view = getChildAt(count - 1); 1232 NestedScrollView.LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1233 mTempRect.bottom = view.getBottom() + lp.bottomMargin + getPaddingBottom(); 1234 mTempRect.top = mTempRect.bottom - height; 1235 } 1236 } 1237 1238 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1239 } 1240 1241 /** 1242 * <p>Scrolls the view to make the area defined by <code>top</code> and 1243 * <code>bottom</code> visible. This method attempts to give the focus 1244 * to a component visible in this area. If no component can be focused in 1245 * the new visible area, the focus is reclaimed by this ScrollView.</p> 1246 * 1247 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1248 * to go upward, {@link android.view.View#FOCUS_DOWN} to downward 1249 * @param top the top offset of the new area to be made visible 1250 * @param bottom the bottom offset of the new area to be made visible 1251 * @return true if the key event is consumed by this method, false otherwise 1252 */ 1253 private boolean scrollAndFocus(int direction, int top, int bottom) { 1254 boolean handled = true; 1255 1256 int height = getHeight(); 1257 int containerTop = getScrollY(); 1258 int containerBottom = containerTop + height; 1259 boolean up = direction == View.FOCUS_UP; 1260 1261 View newFocused = findFocusableViewInBounds(up, top, bottom); 1262 if (newFocused == null) { 1263 newFocused = this; 1264 } 1265 1266 if (top >= containerTop && bottom <= containerBottom) { 1267 handled = false; 1268 } else { 1269 int delta = up ? (top - containerTop) : (bottom - containerBottom); 1270 doScrollY(delta); 1271 } 1272 1273 if (newFocused != findFocus()) newFocused.requestFocus(direction); 1274 1275 return handled; 1276 } 1277 1278 /** 1279 * Handle scrolling in response to an up or down arrow click. 1280 * 1281 * @param direction The direction corresponding to the arrow key that was 1282 * pressed 1283 * @return True if we consumed the event, false otherwise 1284 */ 1285 public boolean arrowScroll(int direction) { 1286 View currentFocused = findFocus(); 1287 if (currentFocused == this) currentFocused = null; 1288 1289 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); 1290 1291 final int maxJump = getMaxScrollAmount(); 1292 1293 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { 1294 nextFocused.getDrawingRect(mTempRect); 1295 offsetDescendantRectToMyCoords(nextFocused, mTempRect); 1296 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1297 doScrollY(scrollDelta); 1298 nextFocused.requestFocus(direction); 1299 } else { 1300 // no new focus 1301 int scrollDelta = maxJump; 1302 1303 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { 1304 scrollDelta = getScrollY(); 1305 } else if (direction == View.FOCUS_DOWN) { 1306 if (getChildCount() > 0) { 1307 View child = getChildAt(0); 1308 NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1309 int daBottom = child.getBottom() + lp.bottomMargin; 1310 int screenBottom = getScrollY() + getHeight() - getPaddingBottom(); 1311 scrollDelta = Math.min(daBottom - screenBottom, maxJump); 1312 } 1313 } 1314 if (scrollDelta == 0) { 1315 return false; 1316 } 1317 doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); 1318 } 1319 1320 if (currentFocused != null && currentFocused.isFocused() 1321 && isOffScreen(currentFocused)) { 1322 // previously focused item still has focus and is off screen, give 1323 // it up (take it back to ourselves) 1324 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are 1325 // sure to 1326 // get it) 1327 final int descendantFocusability = getDescendantFocusability(); // save 1328 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 1329 requestFocus(); 1330 setDescendantFocusability(descendantFocusability); // restore 1331 } 1332 return true; 1333 } 1334 1335 /** 1336 * @return whether the descendant of this scroll view is scrolled off 1337 * screen. 1338 */ 1339 private boolean isOffScreen(View descendant) { 1340 return !isWithinDeltaOfScreen(descendant, 0, getHeight()); 1341 } 1342 1343 /** 1344 * @return whether the descendant of this scroll view is within delta 1345 * pixels of being on the screen. 1346 */ 1347 private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { 1348 descendant.getDrawingRect(mTempRect); 1349 offsetDescendantRectToMyCoords(descendant, mTempRect); 1350 1351 return (mTempRect.bottom + delta) >= getScrollY() 1352 && (mTempRect.top - delta) <= (getScrollY() + height); 1353 } 1354 1355 /** 1356 * Smooth scroll by a Y delta 1357 * 1358 * @param delta the number of pixels to scroll by on the Y axis 1359 */ 1360 private void doScrollY(int delta) { 1361 if (delta != 0) { 1362 if (mSmoothScrollingEnabled) { 1363 smoothScrollBy(0, delta); 1364 } else { 1365 scrollBy(0, delta); 1366 } 1367 } 1368 } 1369 1370 /** 1371 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 1372 * 1373 * @param dx the number of pixels to scroll by on the X axis 1374 * @param dy the number of pixels to scroll by on the Y axis 1375 */ 1376 public final void smoothScrollBy(int dx, int dy) { 1377 if (getChildCount() == 0) { 1378 // Nothing to do. 1379 return; 1380 } 1381 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; 1382 if (duration > ANIMATED_SCROLL_GAP) { 1383 View child = getChildAt(0); 1384 NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1385 int childSize = child.getHeight() + lp.topMargin + lp.bottomMargin; 1386 int parentSpace = getHeight() - getPaddingTop() - getPaddingBottom(); 1387 final int scrollY = getScrollY(); 1388 final int maxY = Math.max(0, childSize - parentSpace); 1389 dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; 1390 mLastScrollerY = getScrollY(); 1391 mScroller.startScroll(getScrollX(), scrollY, 0, dy); 1392 ViewCompat.postInvalidateOnAnimation(this); 1393 } else { 1394 if (!mScroller.isFinished()) { 1395 mScroller.abortAnimation(); 1396 } 1397 scrollBy(dx, dy); 1398 } 1399 mLastScroll = AnimationUtils.currentAnimationTimeMillis(); 1400 } 1401 1402 /** 1403 * Like {@link #scrollTo}, but scroll smoothly instead of immediately. 1404 * 1405 * @param x the position where to scroll on the X axis 1406 * @param y the position where to scroll on the Y axis 1407 */ 1408 public final void smoothScrollTo(int x, int y) { 1409 smoothScrollBy(x - getScrollX(), y - getScrollY()); 1410 } 1411 1412 /** 1413 * <p>The scroll range of a scroll view is the overall height of all of its 1414 * children.</p> 1415 * @hide 1416 */ 1417 @RestrictTo(LIBRARY_GROUP) 1418 @Override 1419 public int computeVerticalScrollRange() { 1420 final int count = getChildCount(); 1421 final int parentSpace = getHeight() - getPaddingBottom() - getPaddingTop(); 1422 if (count == 0) { 1423 return parentSpace; 1424 } 1425 1426 View child = getChildAt(0); 1427 NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1428 int scrollRange = child.getBottom() + lp.bottomMargin; 1429 final int scrollY = getScrollY(); 1430 final int overscrollBottom = Math.max(0, scrollRange - parentSpace); 1431 if (scrollY < 0) { 1432 scrollRange -= scrollY; 1433 } else if (scrollY > overscrollBottom) { 1434 scrollRange += scrollY - overscrollBottom; 1435 } 1436 1437 return scrollRange; 1438 } 1439 1440 /** @hide */ 1441 @RestrictTo(LIBRARY_GROUP) 1442 @Override 1443 public int computeVerticalScrollOffset() { 1444 return Math.max(0, super.computeVerticalScrollOffset()); 1445 } 1446 1447 /** @hide */ 1448 @RestrictTo(LIBRARY_GROUP) 1449 @Override 1450 public int computeVerticalScrollExtent() { 1451 return super.computeVerticalScrollExtent(); 1452 } 1453 1454 /** @hide */ 1455 @RestrictTo(LIBRARY_GROUP) 1456 @Override 1457 public int computeHorizontalScrollRange() { 1458 return super.computeHorizontalScrollRange(); 1459 } 1460 1461 /** @hide */ 1462 @RestrictTo(LIBRARY_GROUP) 1463 @Override 1464 public int computeHorizontalScrollOffset() { 1465 return super.computeHorizontalScrollOffset(); 1466 } 1467 1468 /** @hide */ 1469 @RestrictTo(LIBRARY_GROUP) 1470 @Override 1471 public int computeHorizontalScrollExtent() { 1472 return super.computeHorizontalScrollExtent(); 1473 } 1474 1475 @Override 1476 protected void measureChild(View child, int parentWidthMeasureSpec, 1477 int parentHeightMeasureSpec) { 1478 ViewGroup.LayoutParams lp = child.getLayoutParams(); 1479 1480 int childWidthMeasureSpec; 1481 int childHeightMeasureSpec; 1482 1483 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() 1484 + getPaddingRight(), lp.width); 1485 1486 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1487 1488 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1489 } 1490 1491 @Override 1492 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 1493 int parentHeightMeasureSpec, int heightUsed) { 1494 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 1495 1496 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 1497 getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin 1498 + widthUsed, lp.width); 1499 final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 1500 lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); 1501 1502 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1503 } 1504 1505 @Override 1506 public void computeScroll() { 1507 if (mScroller.computeScrollOffset()) { 1508 final int x = mScroller.getCurrX(); 1509 final int y = mScroller.getCurrY(); 1510 1511 int dy = y - mLastScrollerY; 1512 1513 // Dispatch up to parent 1514 if (dispatchNestedPreScroll(0, dy, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)) { 1515 dy -= mScrollConsumed[1]; 1516 } 1517 1518 if (dy != 0) { 1519 final int range = getScrollRange(); 1520 final int oldScrollY = getScrollY(); 1521 1522 overScrollByCompat(0, dy, getScrollX(), oldScrollY, 0, range, 0, 0, false); 1523 1524 final int scrolledDeltaY = getScrollY() - oldScrollY; 1525 final int unconsumedY = dy - scrolledDeltaY; 1526 1527 if (!dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, null, 1528 ViewCompat.TYPE_NON_TOUCH)) { 1529 final int mode = getOverScrollMode(); 1530 final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS 1531 || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 1532 if (canOverscroll) { 1533 ensureGlows(); 1534 if (y <= 0 && oldScrollY > 0) { 1535 mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 1536 } else if (y >= range && oldScrollY < range) { 1537 mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); 1538 } 1539 } 1540 } 1541 } 1542 1543 // Finally update the scroll positions and post an invalidation 1544 mLastScrollerY = y; 1545 ViewCompat.postInvalidateOnAnimation(this); 1546 } else { 1547 // We can't scroll any more, so stop any indirect scrolling 1548 if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { 1549 stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); 1550 } 1551 // and reset the scroller y 1552 mLastScrollerY = 0; 1553 } 1554 } 1555 1556 /** 1557 * Scrolls the view to the given child. 1558 * 1559 * @param child the View to scroll to 1560 */ 1561 private void scrollToChild(View child) { 1562 child.getDrawingRect(mTempRect); 1563 1564 /* Offset from child's local coordinates to ScrollView coordinates */ 1565 offsetDescendantRectToMyCoords(child, mTempRect); 1566 1567 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1568 1569 if (scrollDelta != 0) { 1570 scrollBy(0, scrollDelta); 1571 } 1572 } 1573 1574 /** 1575 * If rect is off screen, scroll just enough to get it (or at least the 1576 * first screen size chunk of it) on screen. 1577 * 1578 * @param rect The rectangle. 1579 * @param immediate True to scroll immediately without animation 1580 * @return true if scrolling was performed 1581 */ 1582 private boolean scrollToChildRect(Rect rect, boolean immediate) { 1583 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); 1584 final boolean scroll = delta != 0; 1585 if (scroll) { 1586 if (immediate) { 1587 scrollBy(0, delta); 1588 } else { 1589 smoothScrollBy(0, delta); 1590 } 1591 } 1592 return scroll; 1593 } 1594 1595 /** 1596 * Compute the amount to scroll in the Y direction in order to get 1597 * a rectangle completely on the screen (or, if taller than the screen, 1598 * at least the first screen size chunk of it). 1599 * 1600 * @param rect The rect. 1601 * @return The scroll delta. 1602 */ 1603 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { 1604 if (getChildCount() == 0) return 0; 1605 1606 int height = getHeight(); 1607 int screenTop = getScrollY(); 1608 int screenBottom = screenTop + height; 1609 int actualScreenBottom = screenBottom; 1610 1611 int fadingEdge = getVerticalFadingEdgeLength(); 1612 1613 // TODO: screenTop should be incremented by fadingEdge * getTopFadingEdgeStrength (but for 1614 // the target scroll distance). 1615 // leave room for top fading edge as long as rect isn't at very top 1616 if (rect.top > 0) { 1617 screenTop += fadingEdge; 1618 } 1619 1620 // TODO: screenBottom should be decremented by fadingEdge * getBottomFadingEdgeStrength (but 1621 // for the target scroll distance). 1622 // leave room for bottom fading edge as long as rect isn't at very bottom 1623 View child = getChildAt(0); 1624 final NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1625 if (rect.bottom < child.getHeight() + lp.topMargin + lp.bottomMargin) { 1626 screenBottom -= fadingEdge; 1627 } 1628 1629 int scrollYDelta = 0; 1630 1631 if (rect.bottom > screenBottom && rect.top > screenTop) { 1632 // need to move down to get it in view: move down just enough so 1633 // that the entire rectangle is in view (or at least the first 1634 // screen size chunk). 1635 1636 if (rect.height() > height) { 1637 // just enough to get screen size chunk on 1638 scrollYDelta += (rect.top - screenTop); 1639 } else { 1640 // get entire rect at bottom of screen 1641 scrollYDelta += (rect.bottom - screenBottom); 1642 } 1643 1644 // make sure we aren't scrolling beyond the end of our content 1645 int bottom = child.getBottom() + lp.bottomMargin; 1646 int distanceToBottom = bottom - actualScreenBottom; 1647 scrollYDelta = Math.min(scrollYDelta, distanceToBottom); 1648 1649 } else if (rect.top < screenTop && rect.bottom < screenBottom) { 1650 // need to move up to get it in view: move up just enough so that 1651 // entire rectangle is in view (or at least the first screen 1652 // size chunk of it). 1653 1654 if (rect.height() > height) { 1655 // screen size chunk 1656 scrollYDelta -= (screenBottom - rect.bottom); 1657 } else { 1658 // entire rect at top 1659 scrollYDelta -= (screenTop - rect.top); 1660 } 1661 1662 // make sure we aren't scrolling any further than the top our content 1663 scrollYDelta = Math.max(scrollYDelta, -getScrollY()); 1664 } 1665 return scrollYDelta; 1666 } 1667 1668 @Override 1669 public void requestChildFocus(View child, View focused) { 1670 if (!mIsLayoutDirty) { 1671 scrollToChild(focused); 1672 } else { 1673 // The child may not be laid out yet, we can't compute the scroll yet 1674 mChildToScrollTo = focused; 1675 } 1676 super.requestChildFocus(child, focused); 1677 } 1678 1679 1680 /** 1681 * When looking for focus in children of a scroll view, need to be a little 1682 * more careful not to give focus to something that is scrolled off screen. 1683 * 1684 * This is more expensive than the default {@link android.view.ViewGroup} 1685 * implementation, otherwise this behavior might have been made the default. 1686 */ 1687 @Override 1688 protected boolean onRequestFocusInDescendants(int direction, 1689 Rect previouslyFocusedRect) { 1690 1691 // convert from forward / backward notation to up / down / left / right 1692 // (ugh). 1693 if (direction == View.FOCUS_FORWARD) { 1694 direction = View.FOCUS_DOWN; 1695 } else if (direction == View.FOCUS_BACKWARD) { 1696 direction = View.FOCUS_UP; 1697 } 1698 1699 final View nextFocus = previouslyFocusedRect == null 1700 ? FocusFinder.getInstance().findNextFocus(this, null, direction) 1701 : FocusFinder.getInstance().findNextFocusFromRect( 1702 this, previouslyFocusedRect, direction); 1703 1704 if (nextFocus == null) { 1705 return false; 1706 } 1707 1708 if (isOffScreen(nextFocus)) { 1709 return false; 1710 } 1711 1712 return nextFocus.requestFocus(direction, previouslyFocusedRect); 1713 } 1714 1715 @Override 1716 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, 1717 boolean immediate) { 1718 // offset into coordinate space of this scroll view 1719 rectangle.offset(child.getLeft() - child.getScrollX(), 1720 child.getTop() - child.getScrollY()); 1721 1722 return scrollToChildRect(rectangle, immediate); 1723 } 1724 1725 @Override 1726 public void requestLayout() { 1727 mIsLayoutDirty = true; 1728 super.requestLayout(); 1729 } 1730 1731 @Override 1732 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1733 super.onLayout(changed, l, t, r, b); 1734 mIsLayoutDirty = false; 1735 // Give a child focus if it needs it 1736 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { 1737 scrollToChild(mChildToScrollTo); 1738 } 1739 mChildToScrollTo = null; 1740 1741 if (!mIsLaidOut) { 1742 // If there is a saved state, scroll to the position saved in that state. 1743 if (mSavedState != null) { 1744 scrollTo(getScrollX(), mSavedState.scrollPosition); 1745 mSavedState = null; 1746 } // mScrollY default value is "0" 1747 1748 // Make sure current scrollY position falls into the scroll range. If it doesn't, 1749 // scroll such that it does. 1750 int childSize = 0; 1751 if (getChildCount() > 0) { 1752 View child = getChildAt(0); 1753 NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1754 childSize = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 1755 } 1756 int parentSpace = b - t - getPaddingTop() - getPaddingBottom(); 1757 int currentScrollY = getScrollY(); 1758 int newScrollY = clamp(currentScrollY, parentSpace, childSize); 1759 if (newScrollY != currentScrollY) { 1760 scrollTo(getScrollX(), newScrollY); 1761 } 1762 } 1763 1764 // Calling this with the present values causes it to re-claim them 1765 scrollTo(getScrollX(), getScrollY()); 1766 mIsLaidOut = true; 1767 } 1768 1769 @Override 1770 public void onAttachedToWindow() { 1771 super.onAttachedToWindow(); 1772 1773 mIsLaidOut = false; 1774 } 1775 1776 @Override 1777 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1778 super.onSizeChanged(w, h, oldw, oldh); 1779 1780 View currentFocused = findFocus(); 1781 if (null == currentFocused || this == currentFocused) { 1782 return; 1783 } 1784 1785 // If the currently-focused view was visible on the screen when the 1786 // screen was at the old height, then scroll the screen to make that 1787 // view visible with the new screen height. 1788 if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { 1789 currentFocused.getDrawingRect(mTempRect); 1790 offsetDescendantRectToMyCoords(currentFocused, mTempRect); 1791 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1792 doScrollY(scrollDelta); 1793 } 1794 } 1795 1796 /** 1797 * Return true if child is a descendant of parent, (or equal to the parent). 1798 */ 1799 private static boolean isViewDescendantOf(View child, View parent) { 1800 if (child == parent) { 1801 return true; 1802 } 1803 1804 final ViewParent theParent = child.getParent(); 1805 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); 1806 } 1807 1808 /** 1809 * Fling the scroll view 1810 * 1811 * @param velocityY The initial velocity in the Y direction. Positive 1812 * numbers mean that the finger/cursor is moving down the screen, 1813 * which means we want to scroll towards the top. 1814 */ 1815 public void fling(int velocityY) { 1816 if (getChildCount() > 0) { 1817 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH); 1818 mScroller.fling(getScrollX(), getScrollY(), // start 1819 0, velocityY, // velocities 1820 0, 0, // x 1821 Integer.MIN_VALUE, Integer.MAX_VALUE, // y 1822 0, 0); // overscroll 1823 mLastScrollerY = getScrollY(); 1824 ViewCompat.postInvalidateOnAnimation(this); 1825 } 1826 } 1827 1828 private void flingWithNestedDispatch(int velocityY) { 1829 final int scrollY = getScrollY(); 1830 final boolean canFling = (scrollY > 0 || velocityY > 0) 1831 && (scrollY < getScrollRange() || velocityY < 0); 1832 if (!dispatchNestedPreFling(0, velocityY)) { 1833 dispatchNestedFling(0, velocityY, canFling); 1834 fling(velocityY); 1835 } 1836 } 1837 1838 private void endDrag() { 1839 mIsBeingDragged = false; 1840 1841 recycleVelocityTracker(); 1842 stopNestedScroll(ViewCompat.TYPE_TOUCH); 1843 1844 if (mEdgeGlowTop != null) { 1845 mEdgeGlowTop.onRelease(); 1846 mEdgeGlowBottom.onRelease(); 1847 } 1848 } 1849 1850 /** 1851 * {@inheritDoc} 1852 * 1853 * <p>This version also clamps the scrolling to the bounds of our child. 1854 */ 1855 @Override 1856 public void scrollTo(int x, int y) { 1857 // we rely on the fact the View.scrollBy calls scrollTo. 1858 if (getChildCount() > 0) { 1859 View child = getChildAt(0); 1860 final NestedScrollView.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1861 int parentSpaceHorizontal = getWidth() - getPaddingLeft() - getPaddingRight(); 1862 int childSizeHorizontal = child.getWidth() + lp.leftMargin + lp.rightMargin; 1863 int parentSpaceVertical = getHeight() - getPaddingTop() - getPaddingBottom(); 1864 int childSizeVertical = child.getHeight() + lp.topMargin + lp.bottomMargin; 1865 x = clamp(x, parentSpaceHorizontal, childSizeHorizontal); 1866 y = clamp(y, parentSpaceVertical, childSizeVertical); 1867 if (x != getScrollX() || y != getScrollY()) { 1868 super.scrollTo(x, y); 1869 } 1870 } 1871 } 1872 1873 private void ensureGlows() { 1874 if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { 1875 if (mEdgeGlowTop == null) { 1876 Context context = getContext(); 1877 mEdgeGlowTop = new EdgeEffect(context); 1878 mEdgeGlowBottom = new EdgeEffect(context); 1879 } 1880 } else { 1881 mEdgeGlowTop = null; 1882 mEdgeGlowBottom = null; 1883 } 1884 } 1885 1886 @Override 1887 public void draw(Canvas canvas) { 1888 super.draw(canvas); 1889 if (mEdgeGlowTop != null) { 1890 final int scrollY = getScrollY(); 1891 if (!mEdgeGlowTop.isFinished()) { 1892 final int restoreCount = canvas.save(); 1893 int width = getWidth(); 1894 int height = getHeight(); 1895 int xTranslation = 0; 1896 int yTranslation = Math.min(0, scrollY); 1897 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) { 1898 width -= getPaddingLeft() + getPaddingRight(); 1899 xTranslation += getPaddingLeft(); 1900 } 1901 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) { 1902 height -= getPaddingTop() + getPaddingBottom(); 1903 yTranslation += getPaddingTop(); 1904 } 1905 canvas.translate(xTranslation, yTranslation); 1906 mEdgeGlowTop.setSize(width, height); 1907 if (mEdgeGlowTop.draw(canvas)) { 1908 ViewCompat.postInvalidateOnAnimation(this); 1909 } 1910 canvas.restoreToCount(restoreCount); 1911 } 1912 if (!mEdgeGlowBottom.isFinished()) { 1913 final int restoreCount = canvas.save(); 1914 int width = getWidth(); 1915 int height = getHeight(); 1916 int xTranslation = 0; 1917 int yTranslation = Math.max(getScrollRange(), scrollY) + height; 1918 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || getClipToPadding()) { 1919 width -= getPaddingLeft() + getPaddingRight(); 1920 xTranslation += getPaddingLeft(); 1921 } 1922 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && getClipToPadding()) { 1923 height -= getPaddingTop() + getPaddingBottom(); 1924 yTranslation -= getPaddingBottom(); 1925 } 1926 canvas.translate(xTranslation - width, yTranslation); 1927 canvas.rotate(180, width, 0); 1928 mEdgeGlowBottom.setSize(width, height); 1929 if (mEdgeGlowBottom.draw(canvas)) { 1930 ViewCompat.postInvalidateOnAnimation(this); 1931 } 1932 canvas.restoreToCount(restoreCount); 1933 } 1934 } 1935 } 1936 1937 private static int clamp(int n, int my, int child) { 1938 if (my >= child || n < 0) { 1939 /* my >= child is this case: 1940 * |--------------- me ---------------| 1941 * |------ child ------| 1942 * or 1943 * |--------------- me ---------------| 1944 * |------ child ------| 1945 * or 1946 * |--------------- me ---------------| 1947 * |------ child ------| 1948 * 1949 * n < 0 is this case: 1950 * |------ me ------| 1951 * |-------- child --------| 1952 * |-- mScrollX --| 1953 */ 1954 return 0; 1955 } 1956 if ((my + n) > child) { 1957 /* this case: 1958 * |------ me ------| 1959 * |------ child ------| 1960 * |-- mScrollX --| 1961 */ 1962 return child - my; 1963 } 1964 return n; 1965 } 1966 1967 @Override 1968 protected void onRestoreInstanceState(Parcelable state) { 1969 if (!(state instanceof SavedState)) { 1970 super.onRestoreInstanceState(state); 1971 return; 1972 } 1973 1974 SavedState ss = (SavedState) state; 1975 super.onRestoreInstanceState(ss.getSuperState()); 1976 mSavedState = ss; 1977 requestLayout(); 1978 } 1979 1980 @Override 1981 protected Parcelable onSaveInstanceState() { 1982 Parcelable superState = super.onSaveInstanceState(); 1983 SavedState ss = new SavedState(superState); 1984 ss.scrollPosition = getScrollY(); 1985 return ss; 1986 } 1987 1988 static class SavedState extends BaseSavedState { 1989 public int scrollPosition; 1990 1991 SavedState(Parcelable superState) { 1992 super(superState); 1993 } 1994 1995 SavedState(Parcel source) { 1996 super(source); 1997 scrollPosition = source.readInt(); 1998 } 1999 2000 @Override 2001 public void writeToParcel(Parcel dest, int flags) { 2002 super.writeToParcel(dest, flags); 2003 dest.writeInt(scrollPosition); 2004 } 2005 2006 @Override 2007 public String toString() { 2008 return "HorizontalScrollView.SavedState{" 2009 + Integer.toHexString(System.identityHashCode(this)) 2010 + " scrollPosition=" + scrollPosition + "}"; 2011 } 2012 2013 public static final Parcelable.Creator<SavedState> CREATOR = 2014 new Parcelable.Creator<SavedState>() { 2015 @Override 2016 public SavedState createFromParcel(Parcel in) { 2017 return new SavedState(in); 2018 } 2019 2020 @Override 2021 public SavedState[] newArray(int size) { 2022 return new SavedState[size]; 2023 } 2024 }; 2025 } 2026 2027 static class AccessibilityDelegate extends AccessibilityDelegateCompat { 2028 @Override 2029 public boolean performAccessibilityAction(View host, int action, Bundle arguments) { 2030 if (super.performAccessibilityAction(host, action, arguments)) { 2031 return true; 2032 } 2033 final NestedScrollView nsvHost = (NestedScrollView) host; 2034 if (!nsvHost.isEnabled()) { 2035 return false; 2036 } 2037 switch (action) { 2038 case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { 2039 final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() 2040 - nsvHost.getPaddingTop(); 2041 final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight, 2042 nsvHost.getScrollRange()); 2043 if (targetScrollY != nsvHost.getScrollY()) { 2044 nsvHost.smoothScrollTo(0, targetScrollY); 2045 return true; 2046 } 2047 } 2048 return false; 2049 case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { 2050 final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() 2051 - nsvHost.getPaddingTop(); 2052 final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0); 2053 if (targetScrollY != nsvHost.getScrollY()) { 2054 nsvHost.smoothScrollTo(0, targetScrollY); 2055 return true; 2056 } 2057 } 2058 return false; 2059 } 2060 return false; 2061 } 2062 2063 @Override 2064 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 2065 super.onInitializeAccessibilityNodeInfo(host, info); 2066 final NestedScrollView nsvHost = (NestedScrollView) host; 2067 info.setClassName(ScrollView.class.getName()); 2068 if (nsvHost.isEnabled()) { 2069 final int scrollRange = nsvHost.getScrollRange(); 2070 if (scrollRange > 0) { 2071 info.setScrollable(true); 2072 if (nsvHost.getScrollY() > 0) { 2073 info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 2074 } 2075 if (nsvHost.getScrollY() < scrollRange) { 2076 info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 2077 } 2078 } 2079 } 2080 } 2081 2082 @Override 2083 public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 2084 super.onInitializeAccessibilityEvent(host, event); 2085 final NestedScrollView nsvHost = (NestedScrollView) host; 2086 event.setClassName(ScrollView.class.getName()); 2087 final boolean scrollable = nsvHost.getScrollRange() > 0; 2088 event.setScrollable(scrollable); 2089 event.setScrollX(nsvHost.getScrollX()); 2090 event.setScrollY(nsvHost.getScrollY()); 2091 AccessibilityRecordCompat.setMaxScrollX(event, nsvHost.getScrollX()); 2092 AccessibilityRecordCompat.setMaxScrollY(event, nsvHost.getScrollRange()); 2093 } 2094 } 2095 } 2096