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