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