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; 21 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 22 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.graphics.Canvas; 26 import android.graphics.Rect; 27 import android.hardware.SensorManager; 28 import android.os.Build; 29 import android.os.Bundle; 30 import android.os.Parcel; 31 import android.os.Parcelable; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.util.TypedValue; 35 import android.view.FocusFinder; 36 import android.view.InputDevice; 37 import android.view.KeyEvent; 38 import android.view.MotionEvent; 39 import android.view.VelocityTracker; 40 import android.view.View; 41 import android.view.ViewConfiguration; 42 import android.view.ViewGroup; 43 import android.view.ViewParent; 44 import android.view.accessibility.AccessibilityEvent; 45 import android.view.animation.AnimationUtils; 46 import android.widget.EdgeEffect; 47 import android.widget.FrameLayout; 48 import android.widget.OverScroller; 49 import android.widget.ScrollView; 50 51 import androidx.annotation.RequiresApi; 52 import androidx.annotation.RestrictTo; 53 import androidx.annotation.VisibleForTesting; 54 import androidx.core.R; 55 import androidx.core.view.AccessibilityDelegateCompat; 56 import androidx.core.view.DifferentialMotionFlingController; 57 import androidx.core.view.DifferentialMotionFlingTarget; 58 import androidx.core.view.MotionEventCompat; 59 import androidx.core.view.NestedScrollingChild3; 60 import androidx.core.view.NestedScrollingChildHelper; 61 import androidx.core.view.NestedScrollingParent3; 62 import androidx.core.view.NestedScrollingParentHelper; 63 import androidx.core.view.ScrollFeedbackProviderCompat; 64 import androidx.core.view.ScrollingView; 65 import androidx.core.view.ViewCompat; 66 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 67 import androidx.core.view.accessibility.AccessibilityRecordCompat; 68 69 import org.jspecify.annotations.NonNull; 70 import org.jspecify.annotations.Nullable; 71 72 import java.util.List; 73 74 /** 75 * NestedScrollView is just like {@link ScrollView}, but it supports acting 76 * as both a nested scrolling parent and child on both new and old versions of Android. 77 * Nested scrolling is enabled by default. 78 */ 79 public class NestedScrollView extends FrameLayout implements NestedScrollingParent3, 80 NestedScrollingChild3, ScrollingView { 81 static final int ANIMATED_SCROLL_GAP = 250; 82 83 static final float MAX_SCROLL_FACTOR = 0.5f; 84 85 private static final String TAG = "NestedScrollView"; 86 private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 250; 87 88 /** 89 * The following are copied from OverScroller to determine how far a fling will go. 90 */ 91 private static final float SCROLL_FRICTION = 0.015f; 92 private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) 93 private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); 94 private final float mPhysicalCoeff; 95 96 /** 97 * When flinging the stretch towards scrolling content, it should destretch quicker than the 98 * fling would normally do. The visual effect of flinging the stretch looks strange as little 99 * appears to happen at first and then when the stretch disappears, the content starts 100 * scrolling quickly. 101 */ 102 private static final float FLING_DESTRETCH_FACTOR = 4f; 103 104 /** 105 * Interface definition for a callback to be invoked when the scroll 106 * X or Y positions of a view change. 107 * 108 * <p>This version of the interface works on all versions of Android, back to API v4.</p> 109 * 110 * @see #setOnScrollChangeListener(OnScrollChangeListener) 111 */ 112 public interface OnScrollChangeListener { 113 /** 114 * Called when the scroll position of a view changes. 115 * @param v The view whose scroll position has changed. 116 * @param scrollX Current horizontal scroll origin. 117 * @param scrollY Current vertical scroll origin. 118 * @param oldScrollX Previous horizontal scroll origin. 119 * @param oldScrollY Previous vertical scroll origin. 120 */ onScrollChange(@onNull NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY)121 void onScrollChange(@NonNull NestedScrollView v, int scrollX, int scrollY, 122 int oldScrollX, int oldScrollY); 123 } 124 125 private long mLastScroll; 126 127 private final Rect mTempRect = new Rect(); 128 private OverScroller mScroller; 129 130 @RestrictTo(LIBRARY) 131 @VisibleForTesting 132 public @NonNull EdgeEffect mEdgeGlowTop; 133 134 @RestrictTo(LIBRARY) 135 @VisibleForTesting 136 public @NonNull EdgeEffect mEdgeGlowBottom; 137 138 @VisibleForTesting 139 @Nullable ScrollFeedbackProviderCompat mScrollFeedbackProvider; 140 141 /** 142 * Position of the last motion event; only used with touch related events (usually to assist 143 * in movement changes in a drag gesture). 144 */ 145 private int mLastMotionY; 146 147 /** 148 * True when the layout has changed but the traversal has not come through yet. 149 * Ideally the view hierarchy would keep track of this for us. 150 */ 151 private boolean mIsLayoutDirty = true; 152 private boolean mIsLaidOut = false; 153 154 /** 155 * The child to give focus to in the event that a child has requested focus while the 156 * layout is dirty. This prevents the scroll from being wrong if the child has not been 157 * laid out before requesting focus. 158 */ 159 private View mChildToScrollTo = null; 160 161 /** 162 * True if the user is currently dragging this ScrollView around. This is 163 * not the same as 'is being flinged', which can be checked by 164 * mScroller.isFinished() (flinging begins when the user lifts their finger). 165 */ 166 private boolean mIsBeingDragged = false; 167 168 /** 169 * Determines speed during touch scrolling 170 */ 171 private VelocityTracker mVelocityTracker; 172 173 /** 174 * When set to true, the scroll view measure its child to make it fill the currently 175 * visible area. 176 */ 177 private boolean mFillViewport; 178 179 /** 180 * Whether arrow scrolling is animated. 181 */ 182 private boolean mSmoothScrollingEnabled = true; 183 184 private int mTouchSlop; 185 private int mMinimumVelocity; 186 private int mMaximumVelocity; 187 188 /** 189 * ID of the active pointer. This is used to retain consistency during 190 * drags/flings if multiple pointers are used. 191 */ 192 private int mActivePointerId = INVALID_POINTER; 193 194 /** 195 * Used during scrolling to retrieve the new offset within the window. Saves memory by saving 196 * x, y changes to this array (0 position = x, 1 position = y) vs. reallocating an x and y 197 * every time. 198 */ 199 private final int[] mScrollOffset = new int[2]; 200 201 /* 202 * Used during scrolling to retrieve the new consumed offset within the window. 203 * Uses same memory saving strategy as mScrollOffset. 204 */ 205 private final int[] mScrollConsumed = new int[2]; 206 207 // Used to track the position of the touch only events relative to the container. 208 private int mNestedYOffset; 209 210 private int mLastScrollerY; 211 212 /** 213 * Sentinel value for no current active pointer. 214 * Used by {@link #mActivePointerId}. 215 */ 216 private static final int INVALID_POINTER = -1; 217 218 private SavedState mSavedState; 219 220 private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); 221 222 private static final int[] SCROLLVIEW_STYLEABLE = new int[] { 223 android.R.attr.fillViewport 224 }; 225 226 private final NestedScrollingParentHelper mParentHelper; 227 private final NestedScrollingChildHelper mChildHelper; 228 229 private float mVerticalScrollFactor; 230 231 private OnScrollChangeListener mOnScrollChangeListener; 232 233 @VisibleForTesting 234 final DifferentialMotionFlingTargetImpl mDifferentialMotionFlingTarget = 235 new DifferentialMotionFlingTargetImpl(); 236 237 @VisibleForTesting 238 DifferentialMotionFlingController mDifferentialMotionFlingController = 239 new DifferentialMotionFlingController(getContext(), mDifferentialMotionFlingTarget); 240 NestedScrollView(@onNull Context context)241 public NestedScrollView(@NonNull Context context) { 242 this(context, null); 243 } 244 NestedScrollView(@onNull Context context, @Nullable AttributeSet attrs)245 public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) { 246 this(context, attrs, R.attr.nestedScrollViewStyle); 247 } 248 NestedScrollView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)249 public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs, 250 int defStyleAttr) { 251 super(context, attrs, defStyleAttr); 252 mEdgeGlowTop = EdgeEffectCompat.create(context, attrs); 253 mEdgeGlowBottom = EdgeEffectCompat.create(context, attrs); 254 255 final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; 256 mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2) 257 * 39.37f // inch/meter 258 * ppi 259 * 0.84f; // look and feel tuning 260 261 initScrollView(); 262 263 final TypedArray a = context.obtainStyledAttributes( 264 attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); 265 266 setFillViewport(a.getBoolean(0, false)); 267 268 a.recycle(); 269 270 mParentHelper = new NestedScrollingParentHelper(this); 271 mChildHelper = new NestedScrollingChildHelper(this); 272 273 // ...because why else would you be using this widget? 274 setNestedScrollingEnabled(true); 275 276 ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); 277 } 278 279 // NestedScrollingChild3 280 281 @Override dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int @Nullable [] offsetInWindow, int type, int @NonNull [] consumed)282 public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, 283 int dyUnconsumed, int @Nullable [] offsetInWindow, int type, int @NonNull [] consumed) { 284 mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, 285 offsetInWindow, type, consumed); 286 } 287 288 // NestedScrollingChild2 289 290 @Override startNestedScroll(int axes, int type)291 public boolean startNestedScroll(int axes, int type) { 292 return mChildHelper.startNestedScroll(axes, type); 293 } 294 295 @Override stopNestedScroll(int type)296 public void stopNestedScroll(int type) { 297 mChildHelper.stopNestedScroll(type); 298 } 299 300 @Override hasNestedScrollingParent(int type)301 public boolean hasNestedScrollingParent(int type) { 302 return mChildHelper.hasNestedScrollingParent(type); 303 } 304 305 @Override dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int @Nullable [] offsetInWindow, int type)306 public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, 307 int dyUnconsumed, int @Nullable [] offsetInWindow, int type) { 308 return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, 309 offsetInWindow, type); 310 } 311 312 @Override dispatchNestedPreScroll( int dx, int dy, int @Nullable [] consumed, int @Nullable [] offsetInWindow, int type )313 public boolean dispatchNestedPreScroll( 314 int dx, 315 int dy, 316 int @Nullable [] consumed, 317 int @Nullable [] offsetInWindow, 318 int type 319 ) { 320 return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); 321 } 322 323 // NestedScrollingChild 324 325 @Override setNestedScrollingEnabled(boolean enabled)326 public void setNestedScrollingEnabled(boolean enabled) { 327 mChildHelper.setNestedScrollingEnabled(enabled); 328 } 329 330 @Override isNestedScrollingEnabled()331 public boolean isNestedScrollingEnabled() { 332 return mChildHelper.isNestedScrollingEnabled(); 333 } 334 335 @Override startNestedScroll(int axes)336 public boolean startNestedScroll(int axes) { 337 return startNestedScroll(axes, ViewCompat.TYPE_TOUCH); 338 } 339 340 @Override stopNestedScroll()341 public void stopNestedScroll() { 342 stopNestedScroll(ViewCompat.TYPE_TOUCH); 343 } 344 345 @Override hasNestedScrollingParent()346 public boolean hasNestedScrollingParent() { 347 return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH); 348 } 349 350 @Override dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int @Nullable [] offsetInWindow)351 public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, 352 int dyUnconsumed, int @Nullable [] offsetInWindow) { 353 return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, 354 offsetInWindow); 355 } 356 357 @Override dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed, int @Nullable [] offsetInWindow)358 public boolean dispatchNestedPreScroll(int dx, int dy, int @Nullable [] consumed, 359 int @Nullable [] offsetInWindow) { 360 return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH); 361 } 362 363 @Override dispatchNestedFling(float velocityX, float velocityY, boolean consumed)364 public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { 365 return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); 366 } 367 368 @Override dispatchNestedPreFling(float velocityX, float velocityY)369 public boolean dispatchNestedPreFling(float velocityX, float velocityY) { 370 return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); 371 } 372 373 // NestedScrollingParent3 374 375 @Override onNestedScroll(@onNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, int @NonNull [] consumed)376 public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, 377 int dxUnconsumed, int dyUnconsumed, int type, int @NonNull [] consumed) { 378 onNestedScrollInternal(dyUnconsumed, type, consumed); 379 } 380 onNestedScrollInternal(int dyUnconsumed, int type, int @Nullable [] consumed)381 private void onNestedScrollInternal(int dyUnconsumed, int type, int @Nullable [] consumed) { 382 final int oldScrollY = getScrollY(); 383 scrollBy(0, dyUnconsumed); 384 final int myConsumed = getScrollY() - oldScrollY; 385 386 if (consumed != null) { 387 consumed[1] += myConsumed; 388 } 389 final int myUnconsumed = dyUnconsumed - myConsumed; 390 391 mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed); 392 } 393 394 // NestedScrollingParent2 395 396 @Override onStartNestedScroll(@onNull View child, @NonNull View target, int axes, int type)397 public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, 398 int type) { 399 return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; 400 } 401 402 @Override onNestedScrollAccepted(@onNull View child, @NonNull View target, int axes, int type)403 public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, 404 int type) { 405 mParentHelper.onNestedScrollAccepted(child, target, axes, type); 406 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type); 407 } 408 409 @Override onStopNestedScroll(@onNull View target, int type)410 public void onStopNestedScroll(@NonNull View target, int type) { 411 mParentHelper.onStopNestedScroll(target, type); 412 stopNestedScroll(type); 413 } 414 415 @Override onNestedScroll(@onNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type)416 public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, 417 int dxUnconsumed, int dyUnconsumed, int type) { 418 onNestedScrollInternal(dyUnconsumed, type, null); 419 } 420 421 @Override onNestedPreScroll(@onNull View target, int dx, int dy, int @NonNull [] consumed, int type)422 public void onNestedPreScroll(@NonNull View target, int dx, int dy, int @NonNull [] consumed, 423 int type) { 424 dispatchNestedPreScroll(dx, dy, consumed, null, type); 425 } 426 427 // NestedScrollingParent 428 429 @Override onStartNestedScroll( @onNull View child, @NonNull View target, int axes)430 public boolean onStartNestedScroll( 431 @NonNull View child, @NonNull View target, int axes) { 432 return onStartNestedScroll(child, target, axes, ViewCompat.TYPE_TOUCH); 433 } 434 435 @Override onNestedScrollAccepted( @onNull View child, @NonNull View target, int axes)436 public void onNestedScrollAccepted( 437 @NonNull View child, @NonNull View target, int axes) { 438 onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH); 439 } 440 441 @Override onStopNestedScroll(@onNull View target)442 public void onStopNestedScroll(@NonNull View target) { 443 onStopNestedScroll(target, ViewCompat.TYPE_TOUCH); 444 } 445 446 @Override onNestedScroll(@onNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)447 public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, 448 int dxUnconsumed, int dyUnconsumed) { 449 onNestedScrollInternal(dyUnconsumed, ViewCompat.TYPE_TOUCH, null); 450 } 451 452 @Override onNestedPreScroll(@onNull View target, int dx, int dy, int @NonNull [] consumed)453 public void onNestedPreScroll(@NonNull View target, int dx, int dy, int @NonNull [] consumed) { 454 onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH); 455 } 456 457 @Override onNestedFling( @onNull View target, float velocityX, float velocityY, boolean consumed)458 public boolean onNestedFling( 459 @NonNull View target, float velocityX, float velocityY, boolean consumed) { 460 if (!consumed) { 461 dispatchNestedFling(0, velocityY, true); 462 fling((int) velocityY); 463 return true; 464 } 465 return false; 466 } 467 468 @Override onNestedPreFling(@onNull View target, float velocityX, float velocityY)469 public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) { 470 return dispatchNestedPreFling(velocityX, velocityY); 471 } 472 473 @Override getNestedScrollAxes()474 public int getNestedScrollAxes() { 475 return mParentHelper.getNestedScrollAxes(); 476 } 477 478 // ScrollView import 479 480 @Override shouldDelayChildPressedState()481 public boolean shouldDelayChildPressedState() { 482 return true; 483 } 484 485 @Override getTopFadingEdgeStrength()486 protected float getTopFadingEdgeStrength() { 487 if (getChildCount() == 0) { 488 return 0.0f; 489 } 490 491 final int length = getVerticalFadingEdgeLength(); 492 final int scrollY = getScrollY(); 493 if (scrollY < length) { 494 return scrollY / (float) length; 495 } 496 497 return 1.0f; 498 } 499 500 @Override getBottomFadingEdgeStrength()501 protected float getBottomFadingEdgeStrength() { 502 if (getChildCount() == 0) { 503 return 0.0f; 504 } 505 506 View child = getChildAt(0); 507 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 508 final int length = getVerticalFadingEdgeLength(); 509 final int bottomEdge = getHeight() - getPaddingBottom(); 510 final int span = child.getBottom() + lp.bottomMargin - getScrollY() - bottomEdge; 511 if (span < length) { 512 return span / (float) length; 513 } 514 515 return 1.0f; 516 } 517 518 /** 519 * @return The maximum amount this scroll view will scroll in response to 520 * an arrow event. 521 */ getMaxScrollAmount()522 public int getMaxScrollAmount() { 523 return (int) (MAX_SCROLL_FACTOR * getHeight()); 524 } 525 initScrollView()526 private void initScrollView() { 527 mScroller = new OverScroller(getContext()); 528 setFocusable(true); 529 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 530 setWillNotDraw(false); 531 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 532 mTouchSlop = configuration.getScaledTouchSlop(); 533 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 534 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 535 } 536 537 @Override addView(@onNull View child)538 public void addView(@NonNull View child) { 539 if (getChildCount() > 0) { 540 throw new IllegalStateException("ScrollView can host only one direct child"); 541 } 542 543 super.addView(child); 544 } 545 546 @Override addView(View child, int index)547 public void addView(View child, int index) { 548 if (getChildCount() > 0) { 549 throw new IllegalStateException("ScrollView can host only one direct child"); 550 } 551 552 super.addView(child, index); 553 } 554 555 @Override addView(View child, ViewGroup.LayoutParams params)556 public void addView(View child, ViewGroup.LayoutParams params) { 557 if (getChildCount() > 0) { 558 throw new IllegalStateException("ScrollView can host only one direct child"); 559 } 560 561 super.addView(child, params); 562 } 563 564 @Override addView(View child, int index, ViewGroup.LayoutParams params)565 public void addView(View child, int index, ViewGroup.LayoutParams params) { 566 if (getChildCount() > 0) { 567 throw new IllegalStateException("ScrollView can host only one direct child"); 568 } 569 570 super.addView(child, index, params); 571 } 572 573 /** 574 * Register a callback to be invoked when the scroll X or Y positions of 575 * this view change. 576 * <p>This version of the method works on all versions of Android, back to API v4.</p> 577 * 578 * @param l The listener to notify when the scroll X or Y position changes. 579 * @see View#getScrollX() 580 * @see View#getScrollY() 581 */ setOnScrollChangeListener(@ullable OnScrollChangeListener l)582 public void setOnScrollChangeListener(@Nullable OnScrollChangeListener l) { 583 mOnScrollChangeListener = l; 584 } 585 586 /** 587 * @return Returns true this ScrollView can be scrolled 588 */ canScroll()589 private boolean canScroll() { 590 if (getChildCount() > 0) { 591 View child = getChildAt(0); 592 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 593 int childSize = child.getHeight() + lp.topMargin + lp.bottomMargin; 594 int parentSpace = getHeight() - getPaddingTop() - getPaddingBottom(); 595 return childSize > parentSpace; 596 } 597 return false; 598 } 599 600 /** 601 * Indicates whether this ScrollView's content is stretched to fill the viewport. 602 * 603 * @return True if the content fills the viewport, false otherwise. 604 * 605 * @attr name android:fillViewport 606 */ isFillViewport()607 public boolean isFillViewport() { 608 return mFillViewport; 609 } 610 611 /** 612 * Set whether this ScrollView should stretch its content height to fill the viewport or not. 613 * 614 * @param fillViewport True to stretch the content's height to the viewport's 615 * boundaries, false otherwise. 616 * 617 * @attr name android:fillViewport 618 */ setFillViewport(boolean fillViewport)619 public void setFillViewport(boolean fillViewport) { 620 if (fillViewport != mFillViewport) { 621 mFillViewport = fillViewport; 622 requestLayout(); 623 } 624 } 625 626 /** 627 * @return Whether arrow scrolling will animate its transition. 628 */ isSmoothScrollingEnabled()629 public boolean isSmoothScrollingEnabled() { 630 return mSmoothScrollingEnabled; 631 } 632 633 /** 634 * Set whether arrow scrolling will animate its transition. 635 * @param smoothScrollingEnabled whether arrow scrolling will animate its transition 636 */ setSmoothScrollingEnabled(boolean smoothScrollingEnabled)637 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { 638 mSmoothScrollingEnabled = smoothScrollingEnabled; 639 } 640 641 @Override onScrollChanged(int l, int t, int oldl, int oldt)642 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 643 super.onScrollChanged(l, t, oldl, oldt); 644 645 if (mOnScrollChangeListener != null) { 646 mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); 647 } 648 } 649 650 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)651 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 652 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 653 654 if (!mFillViewport) { 655 return; 656 } 657 658 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 659 if (heightMode == MeasureSpec.UNSPECIFIED) { 660 return; 661 } 662 663 if (getChildCount() > 0) { 664 View child = getChildAt(0); 665 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 666 667 int childSize = child.getMeasuredHeight(); 668 int parentSpace = getMeasuredHeight() 669 - getPaddingTop() 670 - getPaddingBottom() 671 - lp.topMargin 672 - lp.bottomMargin; 673 674 if (childSize < parentSpace) { 675 int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 676 getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, 677 lp.width); 678 int childHeightMeasureSpec = 679 MeasureSpec.makeMeasureSpec(parentSpace, MeasureSpec.EXACTLY); 680 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 681 } 682 } 683 } 684 685 @Override dispatchKeyEvent(KeyEvent event)686 public boolean dispatchKeyEvent(KeyEvent event) { 687 // Let the focused view and/or our descendants get the key first 688 return super.dispatchKeyEvent(event) || executeKeyEvent(event); 689 } 690 691 /** 692 * You can call this function yourself to have the scroll view perform 693 * scrolling from a key event, just as if the event had been dispatched to 694 * it by the view hierarchy. 695 * 696 * @param event The key event to execute. 697 * @return Return true if the event was handled, else false. 698 */ executeKeyEvent(@onNull KeyEvent event)699 public boolean executeKeyEvent(@NonNull KeyEvent event) { 700 mTempRect.setEmpty(); 701 702 if (!canScroll()) { 703 if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { 704 View currentFocused = findFocus(); 705 if (currentFocused == this) currentFocused = null; 706 View nextFocused = FocusFinder.getInstance().findNextFocus(this, 707 currentFocused, View.FOCUS_DOWN); 708 return nextFocused != null 709 && nextFocused != this 710 && nextFocused.requestFocus(View.FOCUS_DOWN); 711 } 712 return false; 713 } 714 715 boolean handled = false; 716 if (event.getAction() == KeyEvent.ACTION_DOWN) { 717 switch (event.getKeyCode()) { 718 case KeyEvent.KEYCODE_DPAD_UP: 719 if (event.isAltPressed()) { 720 handled = fullScroll(View.FOCUS_UP); 721 } else { 722 handled = arrowScroll(View.FOCUS_UP); 723 } 724 break; 725 case KeyEvent.KEYCODE_DPAD_DOWN: 726 if (event.isAltPressed()) { 727 handled = fullScroll(View.FOCUS_DOWN); 728 } else { 729 handled = arrowScroll(View.FOCUS_DOWN); 730 } 731 break; 732 case KeyEvent.KEYCODE_PAGE_UP: 733 handled = fullScroll(View.FOCUS_UP); 734 break; 735 case KeyEvent.KEYCODE_PAGE_DOWN: 736 handled = fullScroll(View.FOCUS_DOWN); 737 break; 738 case KeyEvent.KEYCODE_SPACE: 739 pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); 740 break; 741 case KeyEvent.KEYCODE_MOVE_HOME: 742 pageScroll(View.FOCUS_UP); 743 break; 744 case KeyEvent.KEYCODE_MOVE_END: 745 pageScroll(View.FOCUS_DOWN); 746 break; 747 } 748 } 749 750 return handled; 751 } 752 inChild(int x, int y)753 private boolean inChild(int x, int y) { 754 if (getChildCount() > 0) { 755 final int scrollY = getScrollY(); 756 final View child = getChildAt(0); 757 return !(y < child.getTop() - scrollY 758 || y >= child.getBottom() - scrollY 759 || x < child.getLeft() 760 || x >= child.getRight()); 761 } 762 return false; 763 } 764 initOrResetVelocityTracker()765 private void initOrResetVelocityTracker() { 766 if (mVelocityTracker == null) { 767 mVelocityTracker = VelocityTracker.obtain(); 768 } else { 769 mVelocityTracker.clear(); 770 } 771 } 772 initVelocityTrackerIfNotExists()773 private void initVelocityTrackerIfNotExists() { 774 if (mVelocityTracker == null) { 775 mVelocityTracker = VelocityTracker.obtain(); 776 } 777 } 778 recycleVelocityTracker()779 private void recycleVelocityTracker() { 780 if (mVelocityTracker != null) { 781 mVelocityTracker.recycle(); 782 mVelocityTracker = null; 783 } 784 } 785 786 @Override requestDisallowInterceptTouchEvent(boolean disallowIntercept)787 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 788 if (disallowIntercept) { 789 recycleVelocityTracker(); 790 } 791 super.requestDisallowInterceptTouchEvent(disallowIntercept); 792 } 793 794 @Override onInterceptTouchEvent(@onNull MotionEvent ev)795 public boolean onInterceptTouchEvent(@NonNull MotionEvent ev) { 796 /* 797 * This method JUST determines whether we want to intercept the motion. 798 * If we return true, onMotionEvent will be called and we do the actual 799 * scrolling there. 800 */ 801 802 /* 803 * Shortcut the most recurring case: the user is in the dragging 804 * state and they are moving their finger. We want to intercept this 805 * motion. 806 */ 807 final int action = ev.getAction(); 808 if ((action == MotionEvent.ACTION_MOVE) && mIsBeingDragged) { 809 return true; 810 } 811 812 switch (action & MotionEvent.ACTION_MASK) { 813 case MotionEvent.ACTION_MOVE: { 814 /* 815 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 816 * whether the user has moved far enough from their original down touch. 817 */ 818 819 /* 820 * Locally do absolute value. mLastMotionY is set to the y value 821 * of the down event. 822 */ 823 final int activePointerId = mActivePointerId; 824 if (activePointerId == INVALID_POINTER) { 825 // If we don't have a valid id, the touch down wasn't on content. 826 break; 827 } 828 829 final int pointerIndex = ev.findPointerIndex(activePointerId); 830 if (pointerIndex == -1) { 831 Log.e(TAG, "Invalid pointerId=" + activePointerId 832 + " in onInterceptTouchEvent"); 833 break; 834 } 835 836 final int y = (int) ev.getY(pointerIndex); 837 final int yDiff = Math.abs(y - mLastMotionY); 838 if (yDiff > mTouchSlop 839 && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { 840 mIsBeingDragged = true; 841 mLastMotionY = y; 842 initVelocityTrackerIfNotExists(); 843 mVelocityTracker.addMovement(ev); 844 mNestedYOffset = 0; 845 final ViewParent parent = getParent(); 846 if (parent != null) { 847 parent.requestDisallowInterceptTouchEvent(true); 848 } 849 } 850 break; 851 } 852 853 case MotionEvent.ACTION_DOWN: { 854 final int y = (int) ev.getY(); 855 if (!inChild((int) ev.getX(), y)) { 856 mIsBeingDragged = stopGlowAnimations(ev) || !mScroller.isFinished(); 857 recycleVelocityTracker(); 858 break; 859 } 860 861 /* 862 * Remember location of down touch. 863 * ACTION_DOWN always refers to pointer index 0. 864 */ 865 mLastMotionY = y; 866 mActivePointerId = ev.getPointerId(0); 867 868 initOrResetVelocityTracker(); 869 mVelocityTracker.addMovement(ev); 870 /* 871 * If being flinged and user touches the screen, initiate drag; 872 * otherwise don't. mScroller.isFinished should be false when 873 * being flinged. We also want to catch the edge glow and start dragging 874 * if one is being animated. We need to call computeScrollOffset() first so that 875 * isFinished() is correct. 876 */ 877 mScroller.computeScrollOffset(); 878 mIsBeingDragged = stopGlowAnimations(ev) || !mScroller.isFinished(); 879 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); 880 break; 881 } 882 883 case MotionEvent.ACTION_CANCEL: 884 case MotionEvent.ACTION_UP: 885 /* Release the drag */ 886 mIsBeingDragged = false; 887 mActivePointerId = INVALID_POINTER; 888 recycleVelocityTracker(); 889 if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { 890 postInvalidateOnAnimation(); 891 } 892 stopNestedScroll(ViewCompat.TYPE_TOUCH); 893 break; 894 case MotionEvent.ACTION_POINTER_UP: 895 onSecondaryPointerUp(ev); 896 break; 897 } 898 899 /* 900 * The only time we want to intercept motion events is if we are in the 901 * drag mode. 902 */ 903 return mIsBeingDragged; 904 } 905 906 @Override onTouchEvent(@onNull MotionEvent motionEvent)907 public boolean onTouchEvent(@NonNull MotionEvent motionEvent) { 908 initVelocityTrackerIfNotExists(); 909 910 final int actionMasked = motionEvent.getActionMasked(); 911 912 if (actionMasked == MotionEvent.ACTION_DOWN) { 913 mNestedYOffset = 0; 914 } 915 916 MotionEvent velocityTrackerMotionEvent = MotionEvent.obtain(motionEvent); 917 velocityTrackerMotionEvent.offsetLocation(0, mNestedYOffset); 918 919 switch (actionMasked) { 920 case MotionEvent.ACTION_DOWN: { 921 if (getChildCount() == 0) { 922 return false; 923 } 924 925 // If additional fingers touch the screen while a drag is in progress, this block 926 // of code will make sure the drag isn't interrupted. 927 if (mIsBeingDragged) { 928 final ViewParent parent = getParent(); 929 if (parent != null) { 930 parent.requestDisallowInterceptTouchEvent(true); 931 } 932 } 933 934 /* 935 * If being flinged and user touches, stop the fling. isFinished 936 * will be false if being flinged. 937 */ 938 if (!mScroller.isFinished()) { 939 abortAnimatedScroll(); 940 } 941 942 initializeTouchDrag( 943 (int) motionEvent.getY(), 944 motionEvent.getPointerId(0) 945 ); 946 947 break; 948 } 949 950 case MotionEvent.ACTION_MOVE: { 951 final int activePointerIndex = motionEvent.findPointerIndex(mActivePointerId); 952 if (activePointerIndex == -1) { 953 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 954 break; 955 } 956 957 final int y = (int) motionEvent.getY(activePointerIndex); 958 int deltaY = mLastMotionY - y; 959 deltaY -= releaseVerticalGlow(deltaY, motionEvent.getX(activePointerIndex)); 960 961 // Changes to dragged state if delta is greater than the slop (and not in 962 // the dragged state). 963 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 964 final ViewParent parent = getParent(); 965 if (parent != null) { 966 parent.requestDisallowInterceptTouchEvent(true); 967 } 968 mIsBeingDragged = true; 969 if (deltaY > 0) { 970 deltaY -= mTouchSlop; 971 } else { 972 deltaY += mTouchSlop; 973 } 974 } 975 976 if (mIsBeingDragged) { 977 final int x = (int) motionEvent.getX(activePointerIndex); 978 int scrollOffset = 979 scrollBy(deltaY, MotionEvent.AXIS_Y, motionEvent, x, 980 ViewCompat.TYPE_TOUCH, false); 981 // Updates the global positions (used by later move events to properly scroll). 982 mLastMotionY = y - scrollOffset; 983 mNestedYOffset += scrollOffset; 984 } 985 break; 986 } 987 988 case MotionEvent.ACTION_UP: { 989 final VelocityTracker velocityTracker = mVelocityTracker; 990 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 991 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 992 if ((Math.abs(initialVelocity) >= mMinimumVelocity)) { 993 if (!edgeEffectFling(initialVelocity) 994 && !dispatchNestedPreFling(0, -initialVelocity)) { 995 dispatchNestedFling(0, -initialVelocity, true); 996 fling(-initialVelocity); 997 } 998 } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 999 getScrollRange())) { 1000 postInvalidateOnAnimation(); 1001 } 1002 endTouchDrag(); 1003 break; 1004 } 1005 1006 case MotionEvent.ACTION_CANCEL: { 1007 if (mIsBeingDragged && getChildCount() > 0) { 1008 if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 1009 getScrollRange())) { 1010 postInvalidateOnAnimation(); 1011 } 1012 } 1013 endTouchDrag(); 1014 break; 1015 } 1016 1017 case MotionEvent.ACTION_POINTER_DOWN: { 1018 final int index = motionEvent.getActionIndex(); 1019 mLastMotionY = (int) motionEvent.getY(index); 1020 mActivePointerId = motionEvent.getPointerId(index); 1021 break; 1022 } 1023 1024 case MotionEvent.ACTION_POINTER_UP: { 1025 onSecondaryPointerUp(motionEvent); 1026 mLastMotionY = 1027 (int) motionEvent.getY(motionEvent.findPointerIndex(mActivePointerId)); 1028 break; 1029 } 1030 } 1031 1032 if (mVelocityTracker != null) { 1033 mVelocityTracker.addMovement(velocityTrackerMotionEvent); 1034 } 1035 // Returns object back to be re-used by others. 1036 velocityTrackerMotionEvent.recycle(); 1037 1038 return true; 1039 } 1040 initializeTouchDrag(int lastMotionY, int activePointerId)1041 private void initializeTouchDrag(int lastMotionY, int activePointerId) { 1042 mLastMotionY = lastMotionY; 1043 mActivePointerId = activePointerId; 1044 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); 1045 } 1046 1047 // Ends drag in a nested scroll. endTouchDrag()1048 private void endTouchDrag() { 1049 mActivePointerId = INVALID_POINTER; 1050 mIsBeingDragged = false; 1051 1052 recycleVelocityTracker(); 1053 stopNestedScroll(ViewCompat.TYPE_TOUCH); 1054 1055 mEdgeGlowTop.onRelease(); 1056 mEdgeGlowBottom.onRelease(); 1057 } 1058 1059 /** 1060 * Same as {@link #scrollBy(int, int, MotionEvent, int, int, boolean)}, but with no entry for 1061 * the vertical motion axis as well as the {@link MotionEvent}. 1062 * 1063 * <p>Use this method (instead of the other overload) if the {@link MotionEvent} that caused 1064 * this scroll request is not known. 1065 */ scrollBy( int verticalScrollDistance, int x, int touchType, boolean isSourceMouseOrKeyboard )1066 private int scrollBy( 1067 int verticalScrollDistance, 1068 int x, 1069 int touchType, 1070 boolean isSourceMouseOrKeyboard 1071 ) { 1072 return scrollBy(verticalScrollDistance, /* verticalScrollAxis= */ -1, null, x, touchType, 1073 isSourceMouseOrKeyboard); 1074 } 1075 1076 /** 1077 * Handles scroll events for both touch and non-touch events (mouse scroll wheel, 1078 * rotary button, keyboard, etc.). 1079 * 1080 * Note: This function returns the total scroll offset for this scroll event which is required 1081 * for calculating the total scroll between multiple move events (touch). This returned value 1082 * is NOT needed for non-touch events since a scroll is a one time event (vs. touch where a 1083 * drag may be triggered multiple times with the movement of the finger). 1084 * 1085 * @param verticalScrollDistance the amount of distance (in pixels) to scroll vertically. 1086 * @param verticalScrollAxis the motion axis that triggered the vertical scroll. This is not 1087 * always {@link MotionEvent#AXIS_Y}, because there could be other 1088 * axes that trigger a vertical scroll on the view. For example, 1089 * generic motion events reported via {@link MotionEvent#AXIS_SCROLL} 1090 * or {@link MotionEvent#AXIS_VSCROLL}. Use {@code -1} if the vertical 1091 * scroll axis is not known. 1092 * @param ev the {@link MotionEvent} that caused this scroll. {@code null} if the event is not 1093 * known. 1094 * @param x the target location on the x axis. 1095 * @param touchType the {@link ViewCompat.NestedScrollType} for this scroll. 1096 * @param isSourceMouseOrKeyboard whether or not the scroll was caused by a mouse or a keyboard. 1097 */ 1098 // TODO: You should rename this to nestedScrollBy() so it is different from View.scrollBy 1099 @VisibleForTesting scrollBy( int verticalScrollDistance, int verticalScrollAxis, @Nullable MotionEvent ev, int x, @ViewCompat.NestedScrollType int touchType, boolean isSourceMouseOrKeyboard )1100 int scrollBy( 1101 int verticalScrollDistance, 1102 int verticalScrollAxis, 1103 @Nullable MotionEvent ev, 1104 int x, 1105 @ViewCompat.NestedScrollType int touchType, 1106 boolean isSourceMouseOrKeyboard 1107 ) { 1108 int totalScrollOffset = 0; 1109 1110 /* 1111 * Starts nested scrolling for non-touch events (mouse scroll wheel, rotary button, etc.). 1112 * This is in contrast to a touch event which would trigger the start of nested scrolling 1113 * with a touch down event outside of this method, since for a single gesture scrollBy() 1114 * might be called several times for a move event for a single drag gesture. 1115 */ 1116 if (touchType == ViewCompat.TYPE_NON_TOUCH) { 1117 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, touchType); 1118 } 1119 1120 // Dispatches scrolling delta amount available to parent (to consume what it needs). 1121 // Note: The amounts the parent consumes are saved in arrays named mScrollConsumed and 1122 // mScrollConsumed to save space. 1123 if (dispatchNestedPreScroll( 1124 0, 1125 verticalScrollDistance, 1126 mScrollConsumed, 1127 mScrollOffset, 1128 touchType) 1129 ) { 1130 // Deducts the scroll amount (y) consumed by the parent (x in position 0, 1131 // y in position 1). Nested scroll only works with Y position (so we don't use x). 1132 verticalScrollDistance -= mScrollConsumed[1]; 1133 totalScrollOffset += mScrollOffset[1]; 1134 } 1135 1136 // Retrieves the scroll y position (top position of this view) and scroll Y range (how far 1137 // the scroll can go). 1138 final int initialScrollY = getScrollY(); 1139 final int scrollRangeY = getScrollRange(); 1140 1141 // Overscroll is for adding animations at the top/bottom of a view when the user scrolls 1142 // beyond the beginning/end of the view. Overscroll is not used with a mouse. 1143 boolean canOverscroll = canOverScroll() && !isSourceMouseOrKeyboard; 1144 1145 // Scrolls content in the current View, but clamps it if it goes too far. 1146 boolean hitScrollBarrier = 1147 overScrollByCompat( 1148 0, 1149 verticalScrollDistance, 1150 0, 1151 initialScrollY, 1152 0, 1153 scrollRangeY, 1154 0, 1155 0, 1156 true 1157 ) && !hasNestedScrollingParent(touchType); 1158 1159 // The position may have been adjusted in the previous call, so we must revise our values. 1160 final int scrollYDelta = getScrollY() - initialScrollY; 1161 if (ev != null && scrollYDelta != 0) { 1162 getScrollFeedbackProvider().onScrollProgress( 1163 ev.getDeviceId(), ev.getSource(), verticalScrollAxis, scrollYDelta); 1164 } 1165 final int unconsumedY = verticalScrollDistance - scrollYDelta; 1166 1167 // Reset the Y consumed scroll to zero 1168 mScrollConsumed[1] = 0; 1169 1170 // Dispatch the unconsumed delta Y to the children to consume. 1171 dispatchNestedScroll( 1172 0, 1173 scrollYDelta, 1174 0, 1175 unconsumedY, 1176 mScrollOffset, 1177 touchType, 1178 mScrollConsumed 1179 ); 1180 1181 totalScrollOffset += mScrollOffset[1]; 1182 1183 // Handle overscroll of the children. 1184 verticalScrollDistance -= mScrollConsumed[1]; 1185 int newScrollY = initialScrollY + verticalScrollDistance; 1186 1187 if (newScrollY < 0) { 1188 if (canOverscroll) { 1189 EdgeEffectCompat.onPullDistance( 1190 mEdgeGlowTop, 1191 (float) -verticalScrollDistance / getHeight(), 1192 (float) x / getWidth() 1193 ); 1194 if (ev != null) { 1195 getScrollFeedbackProvider().onScrollLimit( 1196 ev.getDeviceId(), ev.getSource(), verticalScrollAxis, 1197 /* isStart= */ true); 1198 } 1199 1200 if (!mEdgeGlowBottom.isFinished()) { 1201 mEdgeGlowBottom.onRelease(); 1202 } 1203 } 1204 1205 } else if (newScrollY > scrollRangeY) { 1206 if (canOverscroll) { 1207 EdgeEffectCompat.onPullDistance( 1208 mEdgeGlowBottom, 1209 (float) verticalScrollDistance / getHeight(), 1210 1.f - ((float) x / getWidth()) 1211 ); 1212 if (ev != null) { 1213 getScrollFeedbackProvider().onScrollLimit( 1214 ev.getDeviceId(), ev.getSource(), verticalScrollAxis, 1215 /* isStart= */ false); 1216 } 1217 1218 if (!mEdgeGlowTop.isFinished()) { 1219 mEdgeGlowTop.onRelease(); 1220 } 1221 } 1222 } 1223 1224 if (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished()) { 1225 postInvalidateOnAnimation(); 1226 hitScrollBarrier = false; 1227 } 1228 1229 if (hitScrollBarrier && (touchType == ViewCompat.TYPE_TOUCH)) { 1230 // Break our velocity if we hit a scroll barrier. 1231 if (mVelocityTracker != null) { 1232 mVelocityTracker.clear(); 1233 } 1234 } 1235 1236 /* 1237 * Ends nested scrolling for non-touch events (mouse scroll wheel, rotary button, etc.). 1238 * As noted above, this is in contrast to a touch event. 1239 */ 1240 if (touchType == ViewCompat.TYPE_NON_TOUCH) { 1241 stopNestedScroll(touchType); 1242 1243 // Required for scrolling with Rotary Device stretch top/bottom to work properly 1244 mEdgeGlowTop.onRelease(); 1245 mEdgeGlowBottom.onRelease(); 1246 } 1247 1248 return totalScrollOffset; 1249 } 1250 1251 /** 1252 * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should 1253 * animate with a fling. It will animate with a fling if the velocity will remove the 1254 * EdgeEffect through its normal operation. 1255 * 1256 * @param edgeEffect The EdgeEffect that might absorb the velocity. 1257 * @param velocity The velocity of the fling motion 1258 * @return true if the velocity should be absorbed or false if it should be flung. 1259 */ shouldAbsorb(@onNull EdgeEffect edgeEffect, int velocity)1260 private boolean shouldAbsorb(@NonNull EdgeEffect edgeEffect, int velocity) { 1261 if (velocity > 0) { 1262 return true; 1263 } 1264 float distance = EdgeEffectCompat.getDistance(edgeEffect) * getHeight(); 1265 1266 // This is flinging without the spring, so let's see if it will fling past the overscroll 1267 float flingDistance = getSplineFlingDistance(-velocity); 1268 1269 return flingDistance < distance; 1270 } 1271 1272 /** 1273 * If mTopGlow or mBottomGlow is currently active and the motion will remove some of the 1274 * stretch, this will consume any of unconsumedY that the glow can. If the motion would 1275 * increase the stretch, or the EdgeEffect isn't a stretch, then nothing will be consumed. 1276 * 1277 * @param unconsumedY The vertical delta that might be consumed by the vertical EdgeEffects 1278 * @return The remaining unconsumed delta after the edge effects have consumed. 1279 */ consumeFlingInVerticalStretch(int unconsumedY)1280 int consumeFlingInVerticalStretch(int unconsumedY) { 1281 int height = getHeight(); 1282 if (unconsumedY > 0 && EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0f) { 1283 float deltaDistance = -unconsumedY * FLING_DESTRETCH_FACTOR / height; 1284 int consumed = Math.round(-height / FLING_DESTRETCH_FACTOR 1285 * EdgeEffectCompat.onPullDistance(mEdgeGlowTop, deltaDistance, 0.5f)); 1286 if (consumed != unconsumedY) { 1287 mEdgeGlowTop.finish(); 1288 } 1289 return unconsumedY - consumed; 1290 } 1291 if (unconsumedY < 0 && EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0f) { 1292 float deltaDistance = unconsumedY * FLING_DESTRETCH_FACTOR / height; 1293 int consumed = Math.round(height / FLING_DESTRETCH_FACTOR 1294 * EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, deltaDistance, 0.5f)); 1295 if (consumed != unconsumedY) { 1296 mEdgeGlowBottom.finish(); 1297 } 1298 return unconsumedY - consumed; 1299 } 1300 return unconsumedY; 1301 } 1302 1303 /** 1304 * Copied from OverScroller, this returns the distance that a fling with the given velocity 1305 * will go. 1306 * @param velocity The velocity of the fling 1307 * @return The distance that will be traveled by a fling of the given velocity. 1308 */ getSplineFlingDistance(int velocity)1309 private float getSplineFlingDistance(int velocity) { 1310 final double l = 1311 Math.log(INFLEXION * Math.abs(velocity) / (SCROLL_FRICTION * mPhysicalCoeff)); 1312 final double decelMinusOne = DECELERATION_RATE - 1.0; 1313 return (float) (SCROLL_FRICTION * mPhysicalCoeff 1314 * Math.exp(DECELERATION_RATE / decelMinusOne * l)); 1315 } 1316 edgeEffectFling(int velocityY)1317 private boolean edgeEffectFling(int velocityY) { 1318 boolean consumed = true; 1319 if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) { 1320 if (shouldAbsorb(mEdgeGlowTop, velocityY)) { 1321 mEdgeGlowTop.onAbsorb(velocityY); 1322 } else { 1323 fling(-velocityY); 1324 } 1325 } else if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) { 1326 if (shouldAbsorb(mEdgeGlowBottom, -velocityY)) { 1327 mEdgeGlowBottom.onAbsorb(-velocityY); 1328 } else { 1329 fling(-velocityY); 1330 } 1331 } else { 1332 consumed = false; 1333 } 1334 return consumed; 1335 } 1336 1337 /** 1338 * This stops any edge glow animation that is currently running by applying a 1339 * 0 length pull at the displacement given by the provided MotionEvent. On pre-S devices, 1340 * this method does nothing, allowing any animating edge effect to continue animating and 1341 * returning <code>false</code> always. 1342 * 1343 * @param e The motion event to use to indicate the finger position for the displacement of 1344 * the current pull. 1345 * @return <code>true</code> if any edge effect had an existing effect to be drawn ond the 1346 * animation was stopped or <code>false</code> if no edge effect had a value to display. 1347 */ stopGlowAnimations(MotionEvent e)1348 private boolean stopGlowAnimations(MotionEvent e) { 1349 boolean stopped = false; 1350 if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) { 1351 EdgeEffectCompat.onPullDistance(mEdgeGlowTop, 0, e.getX() / getWidth()); 1352 stopped = true; 1353 } 1354 if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) { 1355 EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, 0, 1 - e.getX() / getWidth()); 1356 stopped = true; 1357 } 1358 return stopped; 1359 } 1360 onSecondaryPointerUp(MotionEvent ev)1361 private void onSecondaryPointerUp(MotionEvent ev) { 1362 final int pointerIndex = ev.getActionIndex(); 1363 final int pointerId = ev.getPointerId(pointerIndex); 1364 if (pointerId == mActivePointerId) { 1365 // This was our active pointer going up. Choose a new 1366 // active pointer and adjust accordingly. 1367 // TODO: Make this decision more intelligent. 1368 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1369 mLastMotionY = (int) ev.getY(newPointerIndex); 1370 mActivePointerId = ev.getPointerId(newPointerIndex); 1371 if (mVelocityTracker != null) { 1372 mVelocityTracker.clear(); 1373 } 1374 } 1375 } 1376 1377 @Override onGenericMotionEvent(@onNull MotionEvent motionEvent)1378 public boolean onGenericMotionEvent(@NonNull MotionEvent motionEvent) { 1379 if (motionEvent.getAction() == MotionEvent.ACTION_SCROLL && !mIsBeingDragged) { 1380 final float verticalScroll; 1381 final int x; 1382 final int axis; 1383 1384 if (MotionEventCompat.isFromSource(motionEvent, InputDevice.SOURCE_CLASS_POINTER)) { 1385 verticalScroll = motionEvent.getAxisValue(MotionEvent.AXIS_VSCROLL); 1386 x = (int) motionEvent.getX(); 1387 axis = MotionEvent.AXIS_VSCROLL; 1388 } else if ( 1389 MotionEventCompat.isFromSource(motionEvent, InputDevice.SOURCE_ROTARY_ENCODER) 1390 ) { 1391 verticalScroll = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); 1392 // Since a Wear rotary event doesn't have a true X and we want to support proper 1393 // overscroll animations, we put the x at the center of the screen. 1394 x = getWidth() / 2; 1395 axis = MotionEvent.AXIS_SCROLL; 1396 } else { 1397 verticalScroll = 0; 1398 x = 0; 1399 axis = 0; 1400 } 1401 1402 if (verticalScroll != 0) { 1403 // Rotary and Mouse scrolls are inverted from a touch scroll. 1404 final int invertedDelta = (int) (verticalScroll * getVerticalScrollFactorCompat()); 1405 1406 final boolean isSourceMouse = 1407 MotionEventCompat.isFromSource(motionEvent, InputDevice.SOURCE_MOUSE); 1408 1409 scrollBy(-invertedDelta, axis, motionEvent, x, ViewCompat.TYPE_NON_TOUCH, 1410 isSourceMouse); 1411 if (axis != 0) { 1412 mDifferentialMotionFlingController.onMotionEvent(motionEvent, axis); 1413 } 1414 1415 return true; 1416 } 1417 } 1418 return false; 1419 } 1420 1421 /** 1422 * Returns true if the NestedScrollView supports over scroll. 1423 */ canOverScroll()1424 private boolean canOverScroll() { 1425 final int mode = getOverScrollMode(); 1426 return mode == OVER_SCROLL_ALWAYS 1427 || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && getScrollRange() > 0); 1428 } 1429 1430 @VisibleForTesting getVerticalScrollFactorCompat()1431 float getVerticalScrollFactorCompat() { 1432 if (mVerticalScrollFactor == 0) { 1433 TypedValue outValue = new TypedValue(); 1434 final Context context = getContext(); 1435 if (!context.getTheme().resolveAttribute( 1436 android.R.attr.listPreferredItemHeight, outValue, true)) { 1437 throw new IllegalStateException( 1438 "Expected theme to define listPreferredItemHeight."); 1439 } 1440 mVerticalScrollFactor = outValue.getDimension( 1441 context.getResources().getDisplayMetrics()); 1442 } 1443 return mVerticalScrollFactor; 1444 } 1445 1446 @Override onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)1447 protected void onOverScrolled(int scrollX, int scrollY, 1448 boolean clampedX, boolean clampedY) { 1449 super.scrollTo(scrollX, scrollY); 1450 } 1451 1452 @SuppressWarnings({"SameParameterValue", "unused"}) overScrollByCompat(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)1453 boolean overScrollByCompat(int deltaX, int deltaY, 1454 int scrollX, int scrollY, 1455 int scrollRangeX, int scrollRangeY, 1456 int maxOverScrollX, int maxOverScrollY, 1457 boolean isTouchEvent) { 1458 1459 final int overScrollMode = getOverScrollMode(); 1460 final boolean canScrollHorizontal = 1461 computeHorizontalScrollRange() > computeHorizontalScrollExtent(); 1462 final boolean canScrollVertical = 1463 computeVerticalScrollRange() > computeVerticalScrollExtent(); 1464 1465 final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS 1466 || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); 1467 final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS 1468 || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); 1469 1470 int newScrollX = scrollX + deltaX; 1471 if (!overScrollHorizontal) { 1472 maxOverScrollX = 0; 1473 } 1474 1475 int newScrollY = scrollY + deltaY; 1476 if (!overScrollVertical) { 1477 maxOverScrollY = 0; 1478 } 1479 1480 // Clamp values if at the limits and record 1481 final int left = -maxOverScrollX; 1482 final int right = maxOverScrollX + scrollRangeX; 1483 final int top = -maxOverScrollY; 1484 final int bottom = maxOverScrollY + scrollRangeY; 1485 1486 boolean clampedX = false; 1487 if (newScrollX > right) { 1488 newScrollX = right; 1489 clampedX = true; 1490 } else if (newScrollX < left) { 1491 newScrollX = left; 1492 clampedX = true; 1493 } 1494 1495 boolean clampedY = false; 1496 if (newScrollY > bottom) { 1497 newScrollY = bottom; 1498 clampedY = true; 1499 } else if (newScrollY < top) { 1500 newScrollY = top; 1501 clampedY = true; 1502 } 1503 1504 if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { 1505 mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); 1506 } 1507 1508 onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); 1509 1510 return clampedX || clampedY; 1511 } 1512 getScrollRange()1513 int getScrollRange() { 1514 int scrollRange = 0; 1515 if (getChildCount() > 0) { 1516 View child = getChildAt(0); 1517 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1518 int childSize = child.getHeight() + lp.topMargin + lp.bottomMargin; 1519 int parentSpace = getHeight() - getPaddingTop() - getPaddingBottom(); 1520 scrollRange = Math.max(0, childSize - parentSpace); 1521 } 1522 return scrollRange; 1523 } 1524 1525 /** 1526 * <p> 1527 * Finds the next focusable component that fits in the specified bounds. 1528 * </p> 1529 * 1530 * @param topFocus look for a candidate is the one at the top of the bounds 1531 * if topFocus is true, or at the bottom of the bounds if topFocus is 1532 * false 1533 * @param top the top offset of the bounds in which a focusable must be 1534 * found 1535 * @param bottom the bottom offset of the bounds in which a focusable must 1536 * be found 1537 * @return the next focusable component in the bounds or null if none can 1538 * be found 1539 */ findFocusableViewInBounds(boolean topFocus, int top, int bottom)1540 private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { 1541 1542 List<View> focusables = getFocusables(View.FOCUS_FORWARD); 1543 View focusCandidate = null; 1544 1545 /* 1546 * A fully contained focusable is one where its top is below the bound's 1547 * top, and its bottom is above the bound's bottom. A partially 1548 * contained focusable is one where some part of it is within the 1549 * bounds, but it also has some part that is not within bounds. A fully contained 1550 * focusable is preferred to a partially contained focusable. 1551 */ 1552 boolean foundFullyContainedFocusable = false; 1553 1554 int count = focusables.size(); 1555 for (int i = 0; i < count; i++) { 1556 View view = focusables.get(i); 1557 int viewTop = view.getTop(); 1558 int viewBottom = view.getBottom(); 1559 1560 if (top < viewBottom && viewTop < bottom) { 1561 /* 1562 * the focusable is in the target area, it is a candidate for 1563 * focusing 1564 */ 1565 1566 final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom); 1567 1568 if (focusCandidate == null) { 1569 /* No candidate, take this one */ 1570 focusCandidate = view; 1571 foundFullyContainedFocusable = viewIsFullyContained; 1572 } else { 1573 final boolean viewIsCloserToBoundary = 1574 (topFocus && viewTop < focusCandidate.getTop()) 1575 || (!topFocus && viewBottom > focusCandidate.getBottom()); 1576 1577 if (foundFullyContainedFocusable) { 1578 if (viewIsFullyContained && viewIsCloserToBoundary) { 1579 /* 1580 * We're dealing with only fully contained views, so 1581 * it has to be closer to the boundary to beat our 1582 * candidate 1583 */ 1584 focusCandidate = view; 1585 } 1586 } else { 1587 if (viewIsFullyContained) { 1588 /* Any fully contained view beats a partially contained view */ 1589 focusCandidate = view; 1590 foundFullyContainedFocusable = true; 1591 } else if (viewIsCloserToBoundary) { 1592 /* 1593 * Partially contained view beats another partially 1594 * contained view if it's closer 1595 */ 1596 focusCandidate = view; 1597 } 1598 } 1599 } 1600 } 1601 } 1602 1603 return focusCandidate; 1604 } 1605 1606 /** 1607 * <p>Handles scrolling in response to a "page up/down" shortcut press. This 1608 * method will scroll the view by one page up or down and give the focus 1609 * to the topmost/bottommost component in the new visible area. If no 1610 * component is a good candidate for focus, this scrollview reclaims the 1611 * focus.</p> 1612 * 1613 * @param direction the scroll direction: {@link View#FOCUS_UP} 1614 * to go one page up or 1615 * {@link View#FOCUS_DOWN} to go one page down 1616 * @return true if the key event is consumed by this method, false otherwise 1617 */ pageScroll(int direction)1618 public boolean pageScroll(int direction) { 1619 boolean down = direction == View.FOCUS_DOWN; 1620 int height = getHeight(); 1621 1622 if (down) { 1623 mTempRect.top = getScrollY() + height; 1624 int count = getChildCount(); 1625 if (count > 0) { 1626 View view = getChildAt(count - 1); 1627 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1628 int bottom = view.getBottom() + lp.bottomMargin + getPaddingBottom(); 1629 if (mTempRect.top + height > bottom) { 1630 mTempRect.top = bottom - height; 1631 } 1632 } 1633 } else { 1634 mTempRect.top = getScrollY() - height; 1635 if (mTempRect.top < 0) { 1636 mTempRect.top = 0; 1637 } 1638 } 1639 mTempRect.bottom = mTempRect.top + height; 1640 1641 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1642 } 1643 1644 /** 1645 * <p>Handles scrolling in response to a "home/end" shortcut press. This 1646 * method will scroll the view to the top or bottom and give the focus 1647 * to the topmost/bottommost component in the new visible area. If no 1648 * component is a good candidate for focus, this scrollview reclaims the 1649 * focus.</p> 1650 * 1651 * @param direction the scroll direction: {@link View#FOCUS_UP} 1652 * to go the top of the view or 1653 * {@link View#FOCUS_DOWN} to go the bottom 1654 * @return true if the key event is consumed by this method, false otherwise 1655 */ fullScroll(int direction)1656 public boolean fullScroll(int direction) { 1657 boolean down = direction == View.FOCUS_DOWN; 1658 int height = getHeight(); 1659 1660 mTempRect.top = 0; 1661 mTempRect.bottom = height; 1662 1663 if (down) { 1664 int count = getChildCount(); 1665 if (count > 0) { 1666 View view = getChildAt(count - 1); 1667 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1668 mTempRect.bottom = view.getBottom() + lp.bottomMargin + getPaddingBottom(); 1669 mTempRect.top = mTempRect.bottom - height; 1670 } 1671 } 1672 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1673 } 1674 1675 /** 1676 * <p>Scrolls the view to make the area defined by <code>top</code> and 1677 * <code>bottom</code> visible. This method attempts to give the focus 1678 * to a component visible in this area. If no component can be focused in 1679 * the new visible area, the focus is reclaimed by this ScrollView.</p> 1680 * 1681 * @param direction the scroll direction: {@link View#FOCUS_UP} 1682 * to go upward, {@link View#FOCUS_DOWN} to downward 1683 * @param top the top offset of the new area to be made visible 1684 * @param bottom the bottom offset of the new area to be made visible 1685 * @return true if the key event is consumed by this method, false otherwise 1686 */ scrollAndFocus(int direction, int top, int bottom)1687 private boolean scrollAndFocus(int direction, int top, int bottom) { 1688 boolean handled = true; 1689 1690 int height = getHeight(); 1691 int containerTop = getScrollY(); 1692 int containerBottom = containerTop + height; 1693 boolean up = direction == View.FOCUS_UP; 1694 1695 View newFocused = findFocusableViewInBounds(up, top, bottom); 1696 if (newFocused == null) { 1697 newFocused = this; 1698 } 1699 1700 if (top >= containerTop && bottom <= containerBottom) { 1701 handled = false; 1702 } else { 1703 int delta = up ? (top - containerTop) : (bottom - containerBottom); 1704 scrollBy(delta, 0, ViewCompat.TYPE_NON_TOUCH, true); 1705 } 1706 1707 if (newFocused != findFocus()) newFocused.requestFocus(direction); 1708 1709 return handled; 1710 } 1711 1712 /** 1713 * Handle scrolling in response to an up or down arrow click. 1714 * 1715 * @param direction The direction corresponding to the arrow key that was 1716 * pressed 1717 * @return True if we consumed the event, false otherwise 1718 */ arrowScroll(int direction)1719 public boolean arrowScroll(int direction) { 1720 View currentFocused = findFocus(); 1721 if (currentFocused == this) currentFocused = null; 1722 1723 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); 1724 1725 final int maxJump = getMaxScrollAmount(); 1726 1727 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { 1728 nextFocused.getDrawingRect(mTempRect); 1729 offsetDescendantRectToMyCoords(nextFocused, mTempRect); 1730 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1731 1732 scrollBy(scrollDelta, 0, ViewCompat.TYPE_NON_TOUCH, true); 1733 nextFocused.requestFocus(direction); 1734 1735 } else { 1736 // no new focus 1737 int scrollDelta = maxJump; 1738 1739 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { 1740 scrollDelta = getScrollY(); 1741 } else if (direction == View.FOCUS_DOWN) { 1742 if (getChildCount() > 0) { 1743 View child = getChildAt(0); 1744 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1745 int daBottom = child.getBottom() + lp.bottomMargin; 1746 int screenBottom = getScrollY() + getHeight() - getPaddingBottom(); 1747 scrollDelta = Math.min(daBottom - screenBottom, maxJump); 1748 } 1749 } 1750 if (scrollDelta == 0) { 1751 return false; 1752 } 1753 1754 int finalScrollDelta = direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta; 1755 scrollBy(finalScrollDelta, 0, ViewCompat.TYPE_NON_TOUCH, true); 1756 } 1757 1758 if (currentFocused != null && currentFocused.isFocused() 1759 && isOffScreen(currentFocused)) { 1760 // previously focused item still has focus and is off screen, give 1761 // it up (take it back to ourselves) 1762 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are 1763 // sure to 1764 // get it) 1765 final int descendantFocusability = getDescendantFocusability(); // save 1766 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 1767 requestFocus(); 1768 setDescendantFocusability(descendantFocusability); // restore 1769 } 1770 return true; 1771 } 1772 1773 /** 1774 * @return whether the descendant of this scroll view is scrolled off 1775 * screen. 1776 */ isOffScreen(View descendant)1777 private boolean isOffScreen(View descendant) { 1778 return !isWithinDeltaOfScreen(descendant, 0, getHeight()); 1779 } 1780 1781 /** 1782 * @return whether the descendant of this scroll view is within delta 1783 * pixels of being on the screen. 1784 */ isWithinDeltaOfScreen(View descendant, int delta, int height)1785 private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { 1786 descendant.getDrawingRect(mTempRect); 1787 offsetDescendantRectToMyCoords(descendant, mTempRect); 1788 1789 return (mTempRect.bottom + delta) >= getScrollY() 1790 && (mTempRect.top - delta) <= (getScrollY() + height); 1791 } 1792 1793 /** 1794 * Smooth scroll by a Y delta 1795 * 1796 * @param delta the number of pixels to scroll by on the Y axis 1797 */ doScrollY(int delta)1798 private void doScrollY(int delta) { 1799 if (delta != 0) { 1800 if (mSmoothScrollingEnabled) { 1801 smoothScrollBy(0, delta); 1802 } else { 1803 scrollBy(0, delta); 1804 } 1805 } 1806 } 1807 1808 /** 1809 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 1810 * 1811 * @param dx the number of pixels to scroll by on the X axis 1812 * @param dy the number of pixels to scroll by on the Y axis 1813 */ smoothScrollBy(int dx, int dy)1814 public final void smoothScrollBy(int dx, int dy) { 1815 smoothScrollBy(dx, dy, DEFAULT_SMOOTH_SCROLL_DURATION, false); 1816 } 1817 1818 /** 1819 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 1820 * 1821 * @param dx the number of pixels to scroll by on the X axis 1822 * @param dy the number of pixels to scroll by on the Y axis 1823 * @param scrollDurationMs the duration of the smooth scroll operation in milliseconds 1824 */ smoothScrollBy(int dx, int dy, int scrollDurationMs)1825 public final void smoothScrollBy(int dx, int dy, int scrollDurationMs) { 1826 smoothScrollBy(dx, dy, scrollDurationMs, false); 1827 } 1828 1829 /** 1830 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 1831 * 1832 * @param dx the number of pixels to scroll by on the X axis 1833 * @param dy the number of pixels to scroll by on the Y axis 1834 * @param scrollDurationMs the duration of the smooth scroll operation in milliseconds 1835 * @param withNestedScrolling whether to include nested scrolling operations. 1836 */ smoothScrollBy(int dx, int dy, int scrollDurationMs, boolean withNestedScrolling)1837 private void smoothScrollBy(int dx, int dy, int scrollDurationMs, boolean withNestedScrolling) { 1838 if (getChildCount() == 0) { 1839 // Nothing to do. 1840 return; 1841 } 1842 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; 1843 if (duration > ANIMATED_SCROLL_GAP) { 1844 View child = getChildAt(0); 1845 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1846 int childSize = child.getHeight() + lp.topMargin + lp.bottomMargin; 1847 int parentSpace = getHeight() - getPaddingTop() - getPaddingBottom(); 1848 final int scrollY = getScrollY(); 1849 final int maxY = Math.max(0, childSize - parentSpace); 1850 dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; 1851 mScroller.startScroll(getScrollX(), scrollY, 0, dy, scrollDurationMs); 1852 runAnimatedScroll(withNestedScrolling); 1853 } else { 1854 if (!mScroller.isFinished()) { 1855 abortAnimatedScroll(); 1856 } 1857 scrollBy(dx, dy); 1858 } 1859 mLastScroll = AnimationUtils.currentAnimationTimeMillis(); 1860 } 1861 1862 /** 1863 * Like {@link #scrollTo}, but scroll smoothly instead of immediately. 1864 * 1865 * @param x the position where to scroll on the X axis 1866 * @param y the position where to scroll on the Y axis 1867 */ smoothScrollTo(int x, int y)1868 public final void smoothScrollTo(int x, int y) { 1869 smoothScrollTo(x, y, DEFAULT_SMOOTH_SCROLL_DURATION, false); 1870 } 1871 1872 /** 1873 * Like {@link #scrollTo}, but scroll smoothly instead of immediately. 1874 * 1875 * @param x the position where to scroll on the X axis 1876 * @param y the position where to scroll on the Y axis 1877 * @param scrollDurationMs the duration of the smooth scroll operation in milliseconds 1878 */ smoothScrollTo(int x, int y, int scrollDurationMs)1879 public final void smoothScrollTo(int x, int y, int scrollDurationMs) { 1880 smoothScrollTo(x, y, scrollDurationMs, false); 1881 } 1882 1883 /** 1884 * Like {@link #scrollTo}, but scroll smoothly instead of immediately. 1885 * 1886 * @param x the position where to scroll on the X axis 1887 * @param y the position where to scroll on the Y axis 1888 * @param withNestedScrolling whether to include nested scrolling operations. 1889 */ 1890 // This should be considered private, it is package private to avoid a synthetic ancestor. 1891 @SuppressWarnings("SameParameterValue") smoothScrollTo(int x, int y, boolean withNestedScrolling)1892 void smoothScrollTo(int x, int y, boolean withNestedScrolling) { 1893 smoothScrollTo(x, y, DEFAULT_SMOOTH_SCROLL_DURATION, withNestedScrolling); 1894 } 1895 1896 /** 1897 * Like {@link #scrollTo}, but scroll smoothly instead of immediately. 1898 * 1899 * @param x the position where to scroll on the X axis 1900 * @param y the position where to scroll on the Y axis 1901 * @param scrollDurationMs the duration of the smooth scroll operation in milliseconds 1902 * @param withNestedScrolling whether to include nested scrolling operations. 1903 */ 1904 // This should be considered private, it is package private to avoid a synthetic ancestor. smoothScrollTo(int x, int y, int scrollDurationMs, boolean withNestedScrolling)1905 void smoothScrollTo(int x, int y, int scrollDurationMs, boolean withNestedScrolling) { 1906 smoothScrollBy(x - getScrollX(), y - getScrollY(), scrollDurationMs, withNestedScrolling); 1907 } 1908 1909 /** 1910 * <p>The scroll range of a scroll view is the overall height of all of its 1911 * children.</p> 1912 */ 1913 @RestrictTo(LIBRARY_GROUP_PREFIX) 1914 @Override computeVerticalScrollRange()1915 public int computeVerticalScrollRange() { 1916 final int count = getChildCount(); 1917 final int parentSpace = getHeight() - getPaddingBottom() - getPaddingTop(); 1918 if (count == 0) { 1919 return parentSpace; 1920 } 1921 1922 View child = getChildAt(0); 1923 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1924 int scrollRange = child.getBottom() + lp.bottomMargin; 1925 final int scrollY = getScrollY(); 1926 final int overscrollBottom = Math.max(0, scrollRange - parentSpace); 1927 if (scrollY < 0) { 1928 scrollRange -= scrollY; 1929 } else if (scrollY > overscrollBottom) { 1930 scrollRange += scrollY - overscrollBottom; 1931 } 1932 1933 return scrollRange; 1934 } 1935 1936 @RestrictTo(LIBRARY_GROUP_PREFIX) 1937 @Override computeVerticalScrollOffset()1938 public int computeVerticalScrollOffset() { 1939 return Math.max(0, super.computeVerticalScrollOffset()); 1940 } 1941 1942 @RestrictTo(LIBRARY_GROUP_PREFIX) 1943 @Override computeVerticalScrollExtent()1944 public int computeVerticalScrollExtent() { 1945 return super.computeVerticalScrollExtent(); 1946 } 1947 1948 @RestrictTo(LIBRARY_GROUP_PREFIX) 1949 @Override computeHorizontalScrollRange()1950 public int computeHorizontalScrollRange() { 1951 return super.computeHorizontalScrollRange(); 1952 } 1953 1954 @RestrictTo(LIBRARY_GROUP_PREFIX) 1955 @Override computeHorizontalScrollOffset()1956 public int computeHorizontalScrollOffset() { 1957 return super.computeHorizontalScrollOffset(); 1958 } 1959 1960 @RestrictTo(LIBRARY_GROUP_PREFIX) 1961 @Override computeHorizontalScrollExtent()1962 public int computeHorizontalScrollExtent() { 1963 return super.computeHorizontalScrollExtent(); 1964 } 1965 1966 @Override measureChild(@onNull View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)1967 protected void measureChild(@NonNull View child, int parentWidthMeasureSpec, 1968 int parentHeightMeasureSpec) { 1969 ViewGroup.LayoutParams lp = child.getLayoutParams(); 1970 1971 int childWidthMeasureSpec; 1972 int childHeightMeasureSpec; 1973 1974 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() 1975 + getPaddingRight(), lp.width); 1976 1977 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1978 1979 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1980 } 1981 1982 @Override measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)1983 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 1984 int parentHeightMeasureSpec, int heightUsed) { 1985 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 1986 1987 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 1988 getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin 1989 + widthUsed, lp.width); 1990 final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 1991 lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); 1992 1993 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1994 } 1995 1996 @Override computeScroll()1997 public void computeScroll() { 1998 1999 if (mScroller.isFinished()) { 2000 return; 2001 } 2002 2003 mScroller.computeScrollOffset(); 2004 final int y = mScroller.getCurrY(); 2005 int unconsumed = consumeFlingInVerticalStretch(y - mLastScrollerY); 2006 mLastScrollerY = y; 2007 2008 // Nested Scrolling Pre Pass 2009 mScrollConsumed[1] = 0; 2010 dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, 2011 ViewCompat.TYPE_NON_TOUCH); 2012 unconsumed -= mScrollConsumed[1]; 2013 2014 final int range = getScrollRange(); 2015 2016 if (Build.VERSION.SDK_INT >= 35) { 2017 Api35Impl.setFrameContentVelocity(NestedScrollView.this, 2018 Math.abs(mScroller.getCurrVelocity())); 2019 } 2020 2021 if (unconsumed != 0) { 2022 // Internal Scroll 2023 final int oldScrollY = getScrollY(); 2024 overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, range, 0, 0, false); 2025 final int scrolledByMe = getScrollY() - oldScrollY; 2026 unconsumed -= scrolledByMe; 2027 2028 // Nested Scrolling Post Pass 2029 mScrollConsumed[1] = 0; 2030 dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset, 2031 ViewCompat.TYPE_NON_TOUCH, mScrollConsumed); 2032 unconsumed -= mScrollConsumed[1]; 2033 } 2034 2035 if (unconsumed != 0) { 2036 final int mode = getOverScrollMode(); 2037 final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS 2038 || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 2039 if (canOverscroll) { 2040 if (unconsumed < 0) { 2041 if (mEdgeGlowTop.isFinished()) { 2042 mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 2043 } 2044 } else { 2045 if (mEdgeGlowBottom.isFinished()) { 2046 mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); 2047 } 2048 } 2049 } 2050 abortAnimatedScroll(); 2051 } 2052 2053 if (!mScroller.isFinished()) { 2054 postInvalidateOnAnimation(); 2055 } else { 2056 stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); 2057 } 2058 } 2059 2060 /** 2061 * If either of the vertical edge glows are currently active, this consumes part or all of 2062 * deltaY on the edge glow. 2063 * 2064 * @param deltaY The pointer motion, in pixels, in the vertical direction, positive 2065 * for moving down and negative for moving up. 2066 * @param x The vertical position of the pointer. 2067 * @return The amount of <code>deltaY</code> that has been consumed by the 2068 * edge glow. 2069 */ releaseVerticalGlow(int deltaY, float x)2070 private int releaseVerticalGlow(int deltaY, float x) { 2071 // First allow releasing existing overscroll effect: 2072 float consumed = 0; 2073 float displacement = x / getWidth(); 2074 float pullDistance = (float) deltaY / getHeight(); 2075 if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) { 2076 consumed = -EdgeEffectCompat.onPullDistance(mEdgeGlowTop, -pullDistance, displacement); 2077 if (EdgeEffectCompat.getDistance(mEdgeGlowTop) == 0) { 2078 mEdgeGlowTop.onRelease(); 2079 } 2080 } else if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) { 2081 consumed = EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, pullDistance, 2082 1 - displacement); 2083 if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) == 0) { 2084 mEdgeGlowBottom.onRelease(); 2085 } 2086 } 2087 int pixelsConsumed = Math.round(consumed * getHeight()); 2088 if (pixelsConsumed != 0) { 2089 invalidate(); 2090 } 2091 return pixelsConsumed; 2092 } 2093 runAnimatedScroll(boolean participateInNestedScrolling)2094 private void runAnimatedScroll(boolean participateInNestedScrolling) { 2095 if (participateInNestedScrolling) { 2096 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH); 2097 } else { 2098 stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); 2099 } 2100 mLastScrollerY = getScrollY(); 2101 postInvalidateOnAnimation(); 2102 } 2103 abortAnimatedScroll()2104 private void abortAnimatedScroll() { 2105 mScroller.abortAnimation(); 2106 stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); 2107 } 2108 2109 /** 2110 * Scrolls the view to the given child. 2111 * 2112 * @param child the View to scroll to 2113 */ scrollToChild(View child)2114 private void scrollToChild(View child) { 2115 child.getDrawingRect(mTempRect); 2116 2117 /* Offset from child's local coordinates to ScrollView coordinates */ 2118 offsetDescendantRectToMyCoords(child, mTempRect); 2119 2120 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 2121 2122 if (scrollDelta != 0) { 2123 scrollBy(0, scrollDelta); 2124 } 2125 } 2126 2127 /** 2128 * If rect is off screen, scroll just enough to get it (or at least the 2129 * first screen size chunk of it) on screen. 2130 * 2131 * @param rect The rectangle. 2132 * @param immediate True to scroll immediately without animation 2133 * @return true if scrolling was performed 2134 */ scrollToChildRect(Rect rect, boolean immediate)2135 private boolean scrollToChildRect(Rect rect, boolean immediate) { 2136 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); 2137 final boolean scroll = delta != 0; 2138 if (scroll) { 2139 if (immediate) { 2140 scrollBy(0, delta); 2141 } else { 2142 smoothScrollBy(0, delta); 2143 } 2144 } 2145 return scroll; 2146 } 2147 2148 /** 2149 * Compute the amount to scroll in the Y direction in order to get 2150 * a rectangle completely on the screen (or, if taller than the screen, 2151 * at least the first screen size chunk of it). 2152 * 2153 * @param rect The rect. 2154 * @return The scroll delta. 2155 */ computeScrollDeltaToGetChildRectOnScreen(Rect rect)2156 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { 2157 if (getChildCount() == 0) return 0; 2158 2159 int height = getHeight(); 2160 int screenTop = getScrollY(); 2161 int screenBottom = screenTop + height; 2162 int actualScreenBottom = screenBottom; 2163 2164 int fadingEdge = getVerticalFadingEdgeLength(); 2165 2166 // TODO: screenTop should be incremented by fadingEdge * getTopFadingEdgeStrength (but for 2167 // the target scroll distance). 2168 // leave room for top fading edge as long as rect isn't at very top 2169 if (rect.top > 0) { 2170 screenTop += fadingEdge; 2171 } 2172 2173 // TODO: screenBottom should be decremented by fadingEdge * getBottomFadingEdgeStrength (but 2174 // for the target scroll distance). 2175 // leave room for bottom fading edge as long as rect isn't at very bottom 2176 View child = getChildAt(0); 2177 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 2178 if (rect.bottom < child.getHeight() + lp.topMargin + lp.bottomMargin) { 2179 screenBottom -= fadingEdge; 2180 } 2181 2182 int scrollYDelta = 0; 2183 2184 if (rect.bottom > screenBottom && rect.top > screenTop) { 2185 // need to move down to get it in view: move down just enough so 2186 // that the entire rectangle is in view (or at least the first 2187 // screen size chunk). 2188 2189 if (rect.height() > height) { 2190 // just enough to get screen size chunk on 2191 scrollYDelta += (rect.top - screenTop); 2192 } else { 2193 // get entire rect at bottom of screen 2194 scrollYDelta += (rect.bottom - screenBottom); 2195 } 2196 2197 // make sure we aren't scrolling beyond the end of our content 2198 int bottom = child.getBottom() + lp.bottomMargin; 2199 int distanceToBottom = bottom - actualScreenBottom; 2200 scrollYDelta = Math.min(scrollYDelta, distanceToBottom); 2201 2202 } else if (rect.top < screenTop && rect.bottom < screenBottom) { 2203 // need to move up to get it in view: move up just enough so that 2204 // entire rectangle is in view (or at least the first screen 2205 // size chunk of it). 2206 2207 if (rect.height() > height) { 2208 // screen size chunk 2209 scrollYDelta -= (screenBottom - rect.bottom); 2210 } else { 2211 // entire rect at top 2212 scrollYDelta -= (screenTop - rect.top); 2213 } 2214 2215 // make sure we aren't scrolling any further than the top our content 2216 scrollYDelta = Math.max(scrollYDelta, -getScrollY()); 2217 } 2218 return scrollYDelta; 2219 } 2220 2221 @Override requestChildFocus(View child, View focused)2222 public void requestChildFocus(View child, View focused) { 2223 if (!mIsLayoutDirty) { 2224 scrollToChild(focused); 2225 } else { 2226 // The child may not be laid out yet, we can't compute the scroll yet 2227 mChildToScrollTo = focused; 2228 } 2229 super.requestChildFocus(child, focused); 2230 } 2231 2232 2233 /** 2234 * When looking for focus in children of a scroll view, need to be a little 2235 * more careful not to give focus to something that is scrolled off screen. 2236 * 2237 * This is more expensive than the default {@link ViewGroup} 2238 * implementation, otherwise this behavior might have been made the default. 2239 */ 2240 @Override onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)2241 protected boolean onRequestFocusInDescendants(int direction, 2242 Rect previouslyFocusedRect) { 2243 2244 // convert from forward / backward notation to up / down / left / right 2245 // (ugh). 2246 if (direction == View.FOCUS_FORWARD) { 2247 direction = View.FOCUS_DOWN; 2248 } else if (direction == View.FOCUS_BACKWARD) { 2249 direction = View.FOCUS_UP; 2250 } 2251 2252 final View nextFocus = previouslyFocusedRect == null 2253 ? FocusFinder.getInstance().findNextFocus(this, null, direction) 2254 : FocusFinder.getInstance().findNextFocusFromRect( 2255 this, previouslyFocusedRect, direction); 2256 2257 if (nextFocus == null) { 2258 return false; 2259 } 2260 2261 if (isOffScreen(nextFocus)) { 2262 return false; 2263 } 2264 2265 return nextFocus.requestFocus(direction, previouslyFocusedRect); 2266 } 2267 2268 @Override requestChildRectangleOnScreen(@onNull View child, Rect rectangle, boolean immediate)2269 public boolean requestChildRectangleOnScreen(@NonNull View child, Rect rectangle, 2270 boolean immediate) { 2271 // offset into coordinate space of this scroll view 2272 rectangle.offset(child.getLeft() - child.getScrollX(), 2273 child.getTop() - child.getScrollY()); 2274 2275 return scrollToChildRect(rectangle, immediate); 2276 } 2277 2278 @Override requestLayout()2279 public void requestLayout() { 2280 mIsLayoutDirty = true; 2281 super.requestLayout(); 2282 } 2283 2284 @Override onLayout(boolean changed, int l, int t, int r, int b)2285 protected void onLayout(boolean changed, int l, int t, int r, int b) { 2286 super.onLayout(changed, l, t, r, b); 2287 mIsLayoutDirty = false; 2288 // Give a child focus if it needs it 2289 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { 2290 scrollToChild(mChildToScrollTo); 2291 } 2292 mChildToScrollTo = null; 2293 2294 if (!mIsLaidOut) { 2295 // If there is a saved state, scroll to the position saved in that state. 2296 if (mSavedState != null) { 2297 scrollTo(getScrollX(), mSavedState.scrollPosition); 2298 mSavedState = null; 2299 } // mScrollY default value is "0" 2300 2301 // Make sure current scrollY position falls into the scroll range. If it doesn't, 2302 // scroll such that it does. 2303 int childSize = 0; 2304 if (getChildCount() > 0) { 2305 View child = getChildAt(0); 2306 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 2307 childSize = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 2308 } 2309 int parentSpace = b - t - getPaddingTop() - getPaddingBottom(); 2310 int currentScrollY = getScrollY(); 2311 int newScrollY = clamp(currentScrollY, parentSpace, childSize); 2312 if (newScrollY != currentScrollY) { 2313 scrollTo(getScrollX(), newScrollY); 2314 } 2315 } 2316 2317 // Calling this with the present values causes it to re-claim them 2318 scrollTo(getScrollX(), getScrollY()); 2319 mIsLaidOut = true; 2320 } 2321 2322 @Override onAttachedToWindow()2323 public void onAttachedToWindow() { 2324 super.onAttachedToWindow(); 2325 2326 mIsLaidOut = false; 2327 } 2328 2329 @Override onSizeChanged(int w, int h, int oldw, int oldh)2330 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 2331 super.onSizeChanged(w, h, oldw, oldh); 2332 2333 View currentFocused = findFocus(); 2334 if (null == currentFocused || this == currentFocused) { 2335 return; 2336 } 2337 2338 // If the currently-focused view was visible on the screen when the 2339 // screen was at the old height, then scroll the screen to make that 2340 // view visible with the new screen height. 2341 if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { 2342 currentFocused.getDrawingRect(mTempRect); 2343 offsetDescendantRectToMyCoords(currentFocused, mTempRect); 2344 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 2345 doScrollY(scrollDelta); 2346 } 2347 } 2348 2349 /** 2350 * Return true if child is a descendant of parent, (or equal to the parent). 2351 */ isViewDescendantOf(View child, View parent)2352 private static boolean isViewDescendantOf(View child, View parent) { 2353 if (child == parent) { 2354 return true; 2355 } 2356 2357 final ViewParent theParent = child.getParent(); 2358 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); 2359 } 2360 2361 /** 2362 * Fling the scroll view 2363 * 2364 * @param velocityY The initial velocity in the Y direction. Positive 2365 * numbers mean that the finger/cursor is moving down the screen, 2366 * which means we want to scroll towards the top. 2367 */ fling(int velocityY)2368 public void fling(int velocityY) { 2369 if (getChildCount() > 0) { 2370 2371 mScroller.fling(getScrollX(), getScrollY(), // start 2372 0, velocityY, // velocities 2373 0, 0, // x 2374 Integer.MIN_VALUE, Integer.MAX_VALUE, // y 2375 0, 0); // overscroll 2376 runAnimatedScroll(true); 2377 if (Build.VERSION.SDK_INT >= 35) { 2378 Api35Impl.setFrameContentVelocity(NestedScrollView.this, 2379 Math.abs(mScroller.getCurrVelocity())); 2380 } 2381 } 2382 } 2383 2384 /** 2385 * {@inheritDoc} 2386 * 2387 * <p>This version also clamps the scrolling to the bounds of our child. 2388 */ 2389 @Override scrollTo(int x, int y)2390 public void scrollTo(int x, int y) { 2391 // we rely on the fact the View.scrollBy calls scrollTo. 2392 if (getChildCount() > 0) { 2393 View child = getChildAt(0); 2394 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 2395 int parentSpaceHorizontal = getWidth() - getPaddingLeft() - getPaddingRight(); 2396 int childSizeHorizontal = child.getWidth() + lp.leftMargin + lp.rightMargin; 2397 int parentSpaceVertical = getHeight() - getPaddingTop() - getPaddingBottom(); 2398 int childSizeVertical = child.getHeight() + lp.topMargin + lp.bottomMargin; 2399 x = clamp(x, parentSpaceHorizontal, childSizeHorizontal); 2400 y = clamp(y, parentSpaceVertical, childSizeVertical); 2401 if (x != getScrollX() || y != getScrollY()) { 2402 super.scrollTo(x, y); 2403 } 2404 } 2405 } 2406 2407 @Override draw(@onNull Canvas canvas)2408 public void draw(@NonNull Canvas canvas) { 2409 super.draw(canvas); 2410 final int scrollY = getScrollY(); 2411 if (!mEdgeGlowTop.isFinished()) { 2412 final int restoreCount = canvas.save(); 2413 int width = getWidth(); 2414 int height = getHeight(); 2415 int xTranslation = 0; 2416 int yTranslation = Math.min(0, scrollY); 2417 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP 2418 || Api21Impl.getClipToPadding(this)) { 2419 width -= getPaddingLeft() + getPaddingRight(); 2420 xTranslation += getPaddingLeft(); 2421 } 2422 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP 2423 && Api21Impl.getClipToPadding(this)) { 2424 height -= getPaddingTop() + getPaddingBottom(); 2425 yTranslation += getPaddingTop(); 2426 } 2427 canvas.translate(xTranslation, yTranslation); 2428 mEdgeGlowTop.setSize(width, height); 2429 if (mEdgeGlowTop.draw(canvas)) { 2430 postInvalidateOnAnimation(); 2431 } 2432 canvas.restoreToCount(restoreCount); 2433 } 2434 if (!mEdgeGlowBottom.isFinished()) { 2435 final int restoreCount = canvas.save(); 2436 int width = getWidth(); 2437 int height = getHeight(); 2438 int xTranslation = 0; 2439 int yTranslation = Math.max(getScrollRange(), scrollY) + height; 2440 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP 2441 || Api21Impl.getClipToPadding(this)) { 2442 width -= getPaddingLeft() + getPaddingRight(); 2443 xTranslation += getPaddingLeft(); 2444 } 2445 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP 2446 && Api21Impl.getClipToPadding(this)) { 2447 height -= getPaddingTop() + getPaddingBottom(); 2448 yTranslation -= getPaddingBottom(); 2449 } 2450 canvas.translate(xTranslation - width, yTranslation); 2451 canvas.rotate(180, width, 0); 2452 mEdgeGlowBottom.setSize(width, height); 2453 if (mEdgeGlowBottom.draw(canvas)) { 2454 postInvalidateOnAnimation(); 2455 } 2456 canvas.restoreToCount(restoreCount); 2457 } 2458 } 2459 clamp(int n, int my, int child)2460 private static int clamp(int n, int my, int child) { 2461 if (my >= child || n < 0) { 2462 /* my >= child is this case: 2463 * |--------------- me ---------------| 2464 * |------ child ------| 2465 * or 2466 * |--------------- me ---------------| 2467 * |------ child ------| 2468 * or 2469 * |--------------- me ---------------| 2470 * |------ child ------| 2471 * 2472 * n < 0 is this case: 2473 * |------ me ------| 2474 * |-------- child --------| 2475 * |-- mScrollX --| 2476 */ 2477 return 0; 2478 } 2479 if ((my + n) > child) { 2480 /* this case: 2481 * |------ me ------| 2482 * |------ child ------| 2483 * |-- mScrollX --| 2484 */ 2485 return child - my; 2486 } 2487 return n; 2488 } 2489 2490 @Override onRestoreInstanceState(Parcelable state)2491 protected void onRestoreInstanceState(Parcelable state) { 2492 if (!(state instanceof SavedState)) { 2493 super.onRestoreInstanceState(state); 2494 return; 2495 } 2496 2497 SavedState ss = (SavedState) state; 2498 super.onRestoreInstanceState(ss.getSuperState()); 2499 mSavedState = ss; 2500 requestLayout(); 2501 } 2502 2503 @Override onSaveInstanceState()2504 protected @NonNull Parcelable onSaveInstanceState() { 2505 Parcelable superState = super.onSaveInstanceState(); 2506 SavedState ss = new SavedState(superState); 2507 ss.scrollPosition = getScrollY(); 2508 return ss; 2509 } 2510 2511 static class SavedState extends BaseSavedState { 2512 public int scrollPosition; 2513 SavedState(Parcelable superState)2514 SavedState(Parcelable superState) { 2515 super(superState); 2516 } 2517 SavedState(Parcel source)2518 SavedState(Parcel source) { 2519 super(source); 2520 scrollPosition = source.readInt(); 2521 } 2522 2523 @Override writeToParcel(Parcel dest, int flags)2524 public void writeToParcel(Parcel dest, int flags) { 2525 super.writeToParcel(dest, flags); 2526 dest.writeInt(scrollPosition); 2527 } 2528 2529 @Override toString()2530 public @NonNull String toString() { 2531 return "HorizontalScrollView.SavedState{" 2532 + Integer.toHexString(System.identityHashCode(this)) 2533 + " scrollPosition=" + scrollPosition + "}"; 2534 } 2535 2536 public static final Creator<SavedState> CREATOR = 2537 new Creator<SavedState>() { 2538 @Override 2539 public SavedState createFromParcel(Parcel in) { 2540 return new SavedState(in); 2541 } 2542 2543 @Override 2544 public SavedState[] newArray(int size) { 2545 return new SavedState[size]; 2546 } 2547 }; 2548 } 2549 2550 static class AccessibilityDelegate extends AccessibilityDelegateCompat { 2551 @Override performAccessibilityAction(View host, int action, Bundle arguments)2552 public boolean performAccessibilityAction(View host, int action, Bundle arguments) { 2553 if (super.performAccessibilityAction(host, action, arguments)) { 2554 return true; 2555 } 2556 final NestedScrollView nsvHost = (NestedScrollView) host; 2557 if (!nsvHost.isEnabled()) { 2558 return false; 2559 } 2560 int height = nsvHost.getHeight(); 2561 Rect rect = new Rect(); 2562 // Gets the visible rect on the screen except for the rotation or scale cases which 2563 // might affect the result. 2564 if (nsvHost.getMatrix().isIdentity() && nsvHost.getGlobalVisibleRect(rect)) { 2565 height = rect.height(); 2566 } 2567 switch (action) { 2568 case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: 2569 case android.R.id.accessibilityActionScrollDown: { 2570 final int viewportHeight = height - nsvHost.getPaddingBottom() 2571 - nsvHost.getPaddingTop(); 2572 final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight, 2573 nsvHost.getScrollRange()); 2574 if (targetScrollY != nsvHost.getScrollY()) { 2575 nsvHost.smoothScrollTo(0, targetScrollY, true); 2576 return true; 2577 } 2578 } 2579 return false; 2580 case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: 2581 case android.R.id.accessibilityActionScrollUp: { 2582 final int viewportHeight = height - nsvHost.getPaddingBottom() 2583 - nsvHost.getPaddingTop(); 2584 final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0); 2585 if (targetScrollY != nsvHost.getScrollY()) { 2586 nsvHost.smoothScrollTo(0, targetScrollY, true); 2587 return true; 2588 } 2589 } 2590 return false; 2591 } 2592 return false; 2593 } 2594 2595 @Override onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info)2596 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 2597 super.onInitializeAccessibilityNodeInfo(host, info); 2598 final NestedScrollView nsvHost = (NestedScrollView) host; 2599 info.setClassName(ScrollView.class.getName()); 2600 if (nsvHost.isEnabled()) { 2601 final int scrollRange = nsvHost.getScrollRange(); 2602 if (scrollRange > 0) { 2603 info.setScrollable(true); 2604 if (nsvHost.getScrollY() > 0) { 2605 info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat 2606 .ACTION_SCROLL_BACKWARD); 2607 info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat 2608 .ACTION_SCROLL_UP); 2609 } 2610 if (nsvHost.getScrollY() < scrollRange) { 2611 info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat 2612 .ACTION_SCROLL_FORWARD); 2613 info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat 2614 .ACTION_SCROLL_DOWN); 2615 } 2616 } 2617 } 2618 } 2619 2620 @Override onInitializeAccessibilityEvent(View host, AccessibilityEvent event)2621 public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 2622 super.onInitializeAccessibilityEvent(host, event); 2623 final NestedScrollView nsvHost = (NestedScrollView) host; 2624 event.setClassName(ScrollView.class.getName()); 2625 final boolean scrollable = nsvHost.getScrollRange() > 0; 2626 event.setScrollable(scrollable); 2627 event.setScrollX(nsvHost.getScrollX()); 2628 event.setScrollY(nsvHost.getScrollY()); 2629 AccessibilityRecordCompat.setMaxScrollX(event, nsvHost.getScrollX()); 2630 AccessibilityRecordCompat.setMaxScrollY(event, nsvHost.getScrollRange()); 2631 } 2632 } 2633 getScrollFeedbackProvider()2634 private ScrollFeedbackProviderCompat getScrollFeedbackProvider() { 2635 if (mScrollFeedbackProvider == null) { 2636 mScrollFeedbackProvider = ScrollFeedbackProviderCompat.createProvider(this); 2637 } 2638 return mScrollFeedbackProvider; 2639 } 2640 2641 class DifferentialMotionFlingTargetImpl implements DifferentialMotionFlingTarget { 2642 @Override startDifferentialMotionFling(float velocity)2643 public boolean startDifferentialMotionFling(float velocity) { 2644 if (velocity == 0) { 2645 return false; 2646 } 2647 stopDifferentialMotionFling(); 2648 fling((int) velocity); 2649 return true; 2650 } 2651 2652 @Override stopDifferentialMotionFling()2653 public void stopDifferentialMotionFling() { 2654 mScroller.abortAnimation(); 2655 } 2656 2657 @Override getScaledScrollFactor()2658 public float getScaledScrollFactor() { 2659 return -getVerticalScrollFactorCompat(); 2660 } 2661 } 2662 2663 @RequiresApi(21) 2664 static class Api21Impl { Api21Impl()2665 private Api21Impl() { 2666 // This class is not instantiable. 2667 } 2668 getClipToPadding(ViewGroup viewGroup)2669 static boolean getClipToPadding(ViewGroup viewGroup) { 2670 return viewGroup.getClipToPadding(); 2671 } 2672 } 2673 2674 @RequiresApi(35) 2675 private static final class Api35Impl { setFrameContentVelocity(View view, float velocity)2676 public static void setFrameContentVelocity(View view, float velocity) { 2677 try { 2678 view.setFrameContentVelocity(velocity); 2679 } catch (LinkageError e) { 2680 // The setFrameContentVelocity method is unavailable on this device. 2681 } 2682 } 2683 } 2684 } 2685