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