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 */ onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY)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 NestedScrollView(@onNull Context context)188 public NestedScrollView(@NonNull Context context) { 189 this(context, null); 190 } 191 NestedScrollView(@onNull Context context, @Nullable AttributeSet attrs)192 public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) { 193 this(context, attrs, 0); 194 } 195 NestedScrollView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)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 startNestedScroll(int axes, int type)220 public boolean startNestedScroll(int axes, int type) { 221 return mChildHelper.startNestedScroll(axes, type); 222 } 223 224 @Override stopNestedScroll(int type)225 public void stopNestedScroll(int type) { 226 mChildHelper.stopNestedScroll(type); 227 } 228 229 @Override hasNestedScrollingParent(int type)230 public boolean hasNestedScrollingParent(int type) { 231 return mChildHelper.hasNestedScrollingParent(type); 232 } 233 234 @Override dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type)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 dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type)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 setNestedScrollingEnabled(boolean enabled)250 public void setNestedScrollingEnabled(boolean enabled) { 251 mChildHelper.setNestedScrollingEnabled(enabled); 252 } 253 254 @Override isNestedScrollingEnabled()255 public boolean isNestedScrollingEnabled() { 256 return mChildHelper.isNestedScrollingEnabled(); 257 } 258 259 @Override startNestedScroll(int axes)260 public boolean startNestedScroll(int axes) { 261 return startNestedScroll(axes, ViewCompat.TYPE_TOUCH); 262 } 263 264 @Override stopNestedScroll()265 public void stopNestedScroll() { 266 stopNestedScroll(ViewCompat.TYPE_TOUCH); 267 } 268 269 @Override hasNestedScrollingParent()270 public boolean hasNestedScrollingParent() { 271 return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH); 272 } 273 274 @Override dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)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 dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)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 dispatchNestedFling(float velocityX, float velocityY, boolean consumed)287 public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { 288 return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); 289 } 290 291 @Override dispatchNestedPreFling(float velocityX, float velocityY)292 public boolean dispatchNestedPreFling(float velocityX, float velocityY) { 293 return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); 294 } 295 296 // NestedScrollingParent2 297 298 @Override onStartNestedScroll(@onNull View child, @NonNull View target, int axes, int type)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 onNestedScrollAccepted(@onNull View child, @NonNull View target, int axes, int type)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 onStopNestedScroll(@onNull View target, int type)312 public void onStopNestedScroll(@NonNull View target, int type) { 313 mParentHelper.onStopNestedScroll(target, type); 314 stopNestedScroll(type); 315 } 316 317 @Override onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type)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 onNestedPreScroll(@onNull View target, int dx, int dy, @NonNull int[] consumed, int type)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 onStartNestedScroll(View child, View target, int nestedScrollAxes)337 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 338 return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH); 339 } 340 341 @Override onNestedScrollAccepted(View child, View target, int nestedScrollAxes)342 public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { 343 onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH); 344 } 345 346 @Override onStopNestedScroll(View target)347 public void onStopNestedScroll(View target) { 348 onStopNestedScroll(target, ViewCompat.TYPE_TOUCH); 349 } 350 351 @Override onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)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 onNestedPreScroll(View target, int dx, int dy, int[] consumed)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 onNestedFling(View target, float velocityX, float velocityY, boolean consumed)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 onNestedPreFling(View target, float velocityX, float velocityY)373 public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 374 return dispatchNestedPreFling(velocityX, velocityY); 375 } 376 377 @Override getNestedScrollAxes()378 public int getNestedScrollAxes() { 379 return mParentHelper.getNestedScrollAxes(); 380 } 381 382 // ScrollView import 383 384 @Override shouldDelayChildPressedState()385 public boolean shouldDelayChildPressedState() { 386 return true; 387 } 388 389 @Override getTopFadingEdgeStrength()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 getBottomFadingEdgeStrength()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 */ getMaxScrollAmount()426 public int getMaxScrollAmount() { 427 return (int) (MAX_SCROLL_FACTOR * getHeight()); 428 } 429 initScrollView()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 addView(View child)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 addView(View child, int index)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 addView(View child, ViewGroup.LayoutParams params)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 addView(View child, int index, ViewGroup.LayoutParams params)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 */ setOnScrollChangeListener(@ullable OnScrollChangeListener l)486 public void setOnScrollChangeListener(@Nullable OnScrollChangeListener l) { 487 mOnScrollChangeListener = l; 488 } 489 490 /** 491 * @return Returns true this ScrollView can be scrolled 492 */ canScroll()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 */ isFillViewport()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 */ setFillViewport(boolean fillViewport)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 */ isSmoothScrollingEnabled()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 */ setSmoothScrollingEnabled(boolean smoothScrollingEnabled)541 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { 542 mSmoothScrollingEnabled = smoothScrollingEnabled; 543 } 544 545 @Override onScrollChanged(int l, int t, int oldl, int oldt)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 onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 dispatchKeyEvent(KeyEvent event)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 */ executeKeyEvent(@onNull KeyEvent event)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 inChild(int x, int y)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 initOrResetVelocityTracker()657 private void initOrResetVelocityTracker() { 658 if (mVelocityTracker == null) { 659 mVelocityTracker = VelocityTracker.obtain(); 660 } else { 661 mVelocityTracker.clear(); 662 } 663 } 664 initVelocityTrackerIfNotExists()665 private void initVelocityTrackerIfNotExists() { 666 if (mVelocityTracker == null) { 667 mVelocityTracker = VelocityTracker.obtain(); 668 } 669 } 670 recycleVelocityTracker()671 private void recycleVelocityTracker() { 672 if (mVelocityTracker != null) { 673 mVelocityTracker.recycle(); 674 mVelocityTracker = null; 675 } 676 } 677 678 @Override requestDisallowInterceptTouchEvent(boolean disallowIntercept)679 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 680 if (disallowIntercept) { 681 recycleVelocityTracker(); 682 } 683 super.requestDisallowInterceptTouchEvent(disallowIntercept); 684 } 685 686 @Override onInterceptTouchEvent(MotionEvent ev)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 onTouchEvent(MotionEvent ev)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 onSecondaryPointerUp(MotionEvent ev)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 onGenericMotionEvent(MotionEvent event)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 getVerticalScrollFactorCompat()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 onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)1015 protected void onOverScrolled(int scrollX, int scrollY, 1016 boolean clampedX, boolean clampedY) { 1017 super.scrollTo(scrollX, scrollY); 1018 } 1019 overScrollByCompat(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)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 getScrollRange()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 */ findFocusableViewInBounds(boolean topFocus, int top, int bottom)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 */ pageScroll(int direction)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 */ fullScroll(int direction)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 */ scrollAndFocus(int direction, int top, int bottom)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 */ arrowScroll(int direction)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 */ isOffScreen(View descendant)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 */ isWithinDeltaOfScreen(View descendant, int delta, int height)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 */ doScrollY(int delta)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 */ smoothScrollBy(int dx, int dy)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 */ smoothScrollTo(int x, int y)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 computeVerticalScrollRange()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 computeVerticalScrollOffset()1443 public int computeVerticalScrollOffset() { 1444 return Math.max(0, super.computeVerticalScrollOffset()); 1445 } 1446 1447 /** @hide */ 1448 @RestrictTo(LIBRARY_GROUP) 1449 @Override computeVerticalScrollExtent()1450 public int computeVerticalScrollExtent() { 1451 return super.computeVerticalScrollExtent(); 1452 } 1453 1454 /** @hide */ 1455 @RestrictTo(LIBRARY_GROUP) 1456 @Override computeHorizontalScrollRange()1457 public int computeHorizontalScrollRange() { 1458 return super.computeHorizontalScrollRange(); 1459 } 1460 1461 /** @hide */ 1462 @RestrictTo(LIBRARY_GROUP) 1463 @Override computeHorizontalScrollOffset()1464 public int computeHorizontalScrollOffset() { 1465 return super.computeHorizontalScrollOffset(); 1466 } 1467 1468 /** @hide */ 1469 @RestrictTo(LIBRARY_GROUP) 1470 @Override computeHorizontalScrollExtent()1471 public int computeHorizontalScrollExtent() { 1472 return super.computeHorizontalScrollExtent(); 1473 } 1474 1475 @Override measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)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 measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)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 computeScroll()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 */ scrollToChild(View child)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 */ scrollToChildRect(Rect rect, boolean immediate)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 */ computeScrollDeltaToGetChildRectOnScreen(Rect rect)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 requestChildFocus(View child, View focused)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 onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)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 requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)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 requestLayout()1726 public void requestLayout() { 1727 mIsLayoutDirty = true; 1728 super.requestLayout(); 1729 } 1730 1731 @Override onLayout(boolean changed, int l, int t, int r, int b)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 onAttachedToWindow()1770 public void onAttachedToWindow() { 1771 super.onAttachedToWindow(); 1772 1773 mIsLaidOut = false; 1774 } 1775 1776 @Override onSizeChanged(int w, int h, int oldw, int oldh)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 */ isViewDescendantOf(View child, View parent)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 */ fling(int velocityY)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 flingWithNestedDispatch(int velocityY)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 endDrag()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 scrollTo(int x, int y)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 ensureGlows()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 draw(Canvas canvas)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 clamp(int n, int my, int child)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 onRestoreInstanceState(Parcelable state)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 onSaveInstanceState()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 SavedState(Parcelable superState)1991 SavedState(Parcelable superState) { 1992 super(superState); 1993 } 1994 SavedState(Parcel source)1995 SavedState(Parcel source) { 1996 super(source); 1997 scrollPosition = source.readInt(); 1998 } 1999 2000 @Override writeToParcel(Parcel dest, int flags)2001 public void writeToParcel(Parcel dest, int flags) { 2002 super.writeToParcel(dest, flags); 2003 dest.writeInt(scrollPosition); 2004 } 2005 2006 @Override toString()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 performAccessibilityAction(View host, int action, Bundle arguments)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 onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info)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 onInitializeAccessibilityEvent(View host, AccessibilityEvent event)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