1 /*
2  * Copyright (C) 2006 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 package android.widget;
18 
19 import static android.view.flags.Flags.viewVelocityApi;
20 
21 import android.annotation.ColorInt;
22 import android.annotation.NonNull;
23 import android.compat.annotation.UnsupportedAppUsage;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.content.res.TypedArray;
27 import android.graphics.Canvas;
28 import android.graphics.Rect;
29 import android.os.Build;
30 import android.os.Build.VERSION_CODES;
31 import android.os.Bundle;
32 import android.os.Parcel;
33 import android.os.Parcelable;
34 import android.os.StrictMode;
35 import android.util.AttributeSet;
36 import android.util.Log;
37 import android.view.FocusFinder;
38 import android.view.HapticScrollFeedbackProvider;
39 import android.view.InputDevice;
40 import android.view.KeyEvent;
41 import android.view.MotionEvent;
42 import android.view.VelocityTracker;
43 import android.view.View;
44 import android.view.ViewConfiguration;
45 import android.view.ViewDebug;
46 import android.view.ViewGroup;
47 import android.view.ViewHierarchyEncoder;
48 import android.view.ViewParent;
49 import android.view.accessibility.AccessibilityEvent;
50 import android.view.accessibility.AccessibilityNodeInfo;
51 import android.view.animation.AnimationUtils;
52 import android.view.flags.Flags;
53 import android.view.inspector.InspectableProperty;
54 
55 import com.android.internal.R;
56 import com.android.internal.annotations.VisibleForTesting;
57 
58 import java.util.List;
59 
60 /**
61  * A view group that allows the view hierarchy placed within it to be scrolled.
62  * Scroll view may have only one direct child placed within it.
63  * To add multiple views within the scroll view, make
64  * the direct child you add a view group, for example {@link LinearLayout}, and
65  * place additional views within that LinearLayout.
66  *
67  * <p>Scroll view supports vertical scrolling only. For horizontal scrolling,
68  * use {@link HorizontalScrollView} instead.</p>
69  *
70  * <p>Never add a {@link androidx.recyclerview.widget.RecyclerView} or {@link ListView} to
71  * a scroll view. Doing so results in poor user interface performance and a poor user
72  * experience.</p>
73  *
74  * <p class="note">
75  * For vertical scrolling, consider {@link androidx.core.widget.NestedScrollView}
76  * instead of scroll view which offers greater user interface flexibility and
77  * support for the material design scrolling patterns.</p>
78  *
79  * <p>Material Design offers guidelines on how the appearance of
80  * <a href="https://material.io/components/">several UI components</a>, including app bars and
81  * banners, should respond to gestures.</p>
82  *
83  * @attr ref android.R.styleable#ScrollView_fillViewport
84  */
85 public class ScrollView extends FrameLayout {
86     static final int ANIMATED_SCROLL_GAP = 250;
87 
88     static final float MAX_SCROLL_FACTOR = 0.5f;
89 
90     private static final String TAG = "ScrollView";
91 
92     /**
93      * When flinging the stretch towards scrolling content, it should destretch quicker than the
94      * fling would normally do. The visual effect of flinging the stretch looks strange as little
95      * appears to happen at first and then when the stretch disappears, the content starts
96      * scrolling quickly.
97      */
98     private static final float FLING_DESTRETCH_FACTOR = 4f;
99 
100     @UnsupportedAppUsage
101     private long mLastScroll;
102 
103     private final Rect mTempRect = new Rect();
104     @UnsupportedAppUsage
105     private OverScroller mScroller;
106     /**
107      * Tracks the state of the top edge glow.
108      *
109      * Even though this field is practically final, we cannot make it final because there are apps
110      * setting it via reflection and they need to keep working until they target Q.
111      * @hide
112      */
113     @NonNull
114     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768600)
115     @VisibleForTesting
116     public EdgeEffect mEdgeGlowTop;
117 
118     /**
119      * Tracks the state of the bottom edge glow.
120      *
121      * Even though this field is practically final, we cannot make it final because there are apps
122      * setting it via reflection and they need to keep working until they target Q.
123      * @hide
124      */
125     @NonNull
126     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769386)
127     @VisibleForTesting
128     public EdgeEffect mEdgeGlowBottom;
129 
130     /**
131      * Position of the last motion event.
132      */
133     @UnsupportedAppUsage
134     private int mLastMotionY;
135 
136     /**
137      * True when the layout has changed but the traversal has not come through yet.
138      * Ideally the view hierarchy would keep track of this for us.
139      */
140     private boolean mIsLayoutDirty = true;
141 
142     /**
143      * The child to give focus to in the event that a child has requested focus while the
144      * layout is dirty. This prevents the scroll from being wrong if the child has not been
145      * laid out before requesting focus.
146      */
147     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769715)
148     private View mChildToScrollTo = null;
149 
150     /**
151      * True if the user is currently dragging this ScrollView around. This is
152      * not the same as 'is being flinged', which can be checked by
153      * mScroller.isFinished() (flinging begins when the user lifts their finger).
154      */
155     @UnsupportedAppUsage
156     private boolean mIsBeingDragged = false;
157 
158     /**
159      * Determines speed during touch scrolling
160      */
161     @UnsupportedAppUsage
162     private VelocityTracker mVelocityTracker;
163 
164     /**
165      * When set to true, the scroll view measure its child to make it fill the currently
166      * visible area.
167      */
168     @ViewDebug.ExportedProperty(category = "layout")
169     private boolean mFillViewport;
170 
171     /**
172      * Whether arrow scrolling is animated.
173      */
174     private boolean mSmoothScrollingEnabled = true;
175 
176     private int mTouchSlop;
177     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124051125)
178     private int mMinimumVelocity;
179     private int mMaximumVelocity;
180 
181     @UnsupportedAppUsage(maxTargetSdk = VERSION_CODES.P, trackingBug = 124050903)
182     private int mOverscrollDistance;
183     @UnsupportedAppUsage(maxTargetSdk = VERSION_CODES.P, trackingBug = 124050903)
184     private int mOverflingDistance;
185 
186     private float mVerticalScrollFactor;
187 
188     /**
189      * ID of the active pointer. This is used to retain consistency during
190      * drags/flings if multiple pointers are used.
191      */
192     private int mActivePointerId = INVALID_POINTER;
193 
194     /**
195      * Used during scrolling to retrieve the new offset within the window.
196      */
197     private final int[] mScrollOffset = new int[2];
198     private final int[] mScrollConsumed = new int[2];
199     private int mNestedYOffset;
200 
201     /**
202      * The StrictMode "critical time span" objects to catch animation
203      * stutters.  Non-null when a time-sensitive animation is
204      * in-flight.  Must call finish() on them when done animating.
205      * These are no-ops on user builds.
206      */
207     private StrictMode.Span mScrollStrictSpan = null;  // aka "drag"
208     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
209     private StrictMode.Span mFlingStrictSpan = null;
210 
211     private DifferentialMotionFlingHelper mDifferentialMotionFlingHelper;
212 
213     private HapticScrollFeedbackProvider mHapticScrollFeedbackProvider;
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 
ScrollView(Context context)223     public ScrollView(Context context) {
224         this(context, null);
225     }
226 
ScrollView(Context context, AttributeSet attrs)227     public ScrollView(Context context, AttributeSet attrs) {
228         this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
229     }
230 
ScrollView(Context context, AttributeSet attrs, int defStyleAttr)231     public ScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
232         this(context, attrs, defStyleAttr, 0);
233     }
234 
ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)235     public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
236         super(context, attrs, defStyleAttr, defStyleRes);
237         mEdgeGlowTop = new EdgeEffect(context, attrs);
238         mEdgeGlowBottom = new EdgeEffect(context, attrs);
239         initScrollView();
240 
241         final TypedArray a = context.obtainStyledAttributes(
242                 attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes);
243         saveAttributeDataForStyleable(context, com.android.internal.R.styleable.ScrollView,
244                 attrs, a, defStyleAttr, defStyleRes);
245 
246         setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
247 
248         a.recycle();
249 
250         if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
251             setRevealOnFocusHint(false);
252         }
253     }
254 
255     @Override
shouldDelayChildPressedState()256     public boolean shouldDelayChildPressedState() {
257         return true;
258     }
259 
260     @Override
getTopFadingEdgeStrength()261     protected float getTopFadingEdgeStrength() {
262         if (getChildCount() == 0) {
263             return 0.0f;
264         }
265 
266         final int length = getVerticalFadingEdgeLength();
267         if (mScrollY < length) {
268             return mScrollY / (float) length;
269         }
270 
271         return 1.0f;
272     }
273 
274     @Override
getBottomFadingEdgeStrength()275     protected float getBottomFadingEdgeStrength() {
276         if (getChildCount() == 0) {
277             return 0.0f;
278         }
279 
280         final int length = getVerticalFadingEdgeLength();
281         final int bottomEdge = getHeight() - mPaddingBottom;
282         final int span = getChildAt(0).getBottom() - mScrollY - bottomEdge;
283         if (span < length) {
284             return span / (float) length;
285         }
286 
287         return 1.0f;
288     }
289 
290     /**
291      * Sets the edge effect color for both top and bottom edge effects.
292      *
293      * @param color The color for the edge effects.
294      * @see #setTopEdgeEffectColor(int)
295      * @see #setBottomEdgeEffectColor(int)
296      * @see #getTopEdgeEffectColor()
297      * @see #getBottomEdgeEffectColor()
298      */
setEdgeEffectColor(@olorInt int color)299     public void setEdgeEffectColor(@ColorInt int color) {
300         setTopEdgeEffectColor(color);
301         setBottomEdgeEffectColor(color);
302     }
303 
304     /**
305      * Sets the bottom edge effect color.
306      *
307      * @param color The color for the bottom edge effect.
308      * @see #setTopEdgeEffectColor(int)
309      * @see #setEdgeEffectColor(int)
310      * @see #getTopEdgeEffectColor()
311      * @see #getBottomEdgeEffectColor()
312      */
setBottomEdgeEffectColor(@olorInt int color)313     public void setBottomEdgeEffectColor(@ColorInt int color) {
314         mEdgeGlowBottom.setColor(color);
315     }
316 
317     /**
318      * Sets the top edge effect color.
319      *
320      * @param color The color for the top edge effect.
321      * @see #setBottomEdgeEffectColor(int)
322      * @see #setEdgeEffectColor(int)
323      * @see #getTopEdgeEffectColor()
324      * @see #getBottomEdgeEffectColor()
325      */
setTopEdgeEffectColor(@olorInt int color)326     public void setTopEdgeEffectColor(@ColorInt int color) {
327         mEdgeGlowTop.setColor(color);
328     }
329 
330     /**
331      * Returns the top edge effect color.
332      *
333      * @return The top edge effect color.
334      * @see #setEdgeEffectColor(int)
335      * @see #setTopEdgeEffectColor(int)
336      * @see #setBottomEdgeEffectColor(int)
337      * @see #getBottomEdgeEffectColor()
338      */
339     @ColorInt
getTopEdgeEffectColor()340     public int getTopEdgeEffectColor() {
341         return mEdgeGlowTop.getColor();
342     }
343 
344     /**
345      * Returns the bottom edge effect color.
346      *
347      * @return The bottom edge effect color.
348      * @see #setEdgeEffectColor(int)
349      * @see #setTopEdgeEffectColor(int)
350      * @see #setBottomEdgeEffectColor(int)
351      * @see #getTopEdgeEffectColor()
352      */
353     @ColorInt
getBottomEdgeEffectColor()354     public int getBottomEdgeEffectColor() {
355         return mEdgeGlowBottom.getColor();
356     }
357 
358     /**
359      * @return The maximum amount this scroll view will scroll in response to
360      *   an arrow event.
361      */
getMaxScrollAmount()362     public int getMaxScrollAmount() {
363         return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop));
364     }
365 
initScrollView()366     private void initScrollView() {
367         mScroller = new OverScroller(getContext());
368         setFocusable(true);
369         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
370         setWillNotDraw(false);
371         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
372         mTouchSlop = configuration.getScaledTouchSlop();
373         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
374         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
375         mOverscrollDistance = configuration.getScaledOverscrollDistance();
376         mOverflingDistance = configuration.getScaledOverflingDistance();
377         mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor();
378     }
379 
380     @Override
addView(View child)381     public void addView(View child) {
382         if (getChildCount() > 0) {
383             throw new IllegalStateException("ScrollView can host only one direct child");
384         }
385 
386         super.addView(child);
387     }
388 
389     @Override
addView(View child, int index)390     public void addView(View child, int index) {
391         if (getChildCount() > 0) {
392             throw new IllegalStateException("ScrollView can host only one direct child");
393         }
394 
395         super.addView(child, index);
396     }
397 
398     @Override
addView(View child, ViewGroup.LayoutParams params)399     public void addView(View child, ViewGroup.LayoutParams params) {
400         if (getChildCount() > 0) {
401             throw new IllegalStateException("ScrollView can host only one direct child");
402         }
403 
404         super.addView(child, params);
405     }
406 
407     @Override
addView(View child, int index, ViewGroup.LayoutParams params)408     public void addView(View child, int index, ViewGroup.LayoutParams params) {
409         if (getChildCount() > 0) {
410             throw new IllegalStateException("ScrollView can host only one direct child");
411         }
412 
413         super.addView(child, index, params);
414     }
415 
416     /**
417      * @return Returns true this ScrollView can be scrolled
418      */
419     @UnsupportedAppUsage
canScroll()420     private boolean canScroll() {
421         View child = getChildAt(0);
422         if (child != null) {
423             int childHeight = child.getHeight();
424             return getHeight() < childHeight + mPaddingTop + mPaddingBottom;
425         }
426         return false;
427     }
428 
429     /**
430      * Indicates whether this ScrollView's content is stretched to fill the viewport.
431      *
432      * @return True if the content fills the viewport, false otherwise.
433      *
434      * @attr ref android.R.styleable#ScrollView_fillViewport
435      */
436     @InspectableProperty
isFillViewport()437     public boolean isFillViewport() {
438         return mFillViewport;
439     }
440 
441     /**
442      * Indicates this ScrollView whether it should stretch its content height to fill
443      * the viewport or not.
444      *
445      * @param fillViewport True to stretch the content's height to the viewport's
446      *        boundaries, false otherwise.
447      *
448      * @attr ref android.R.styleable#ScrollView_fillViewport
449      */
setFillViewport(boolean fillViewport)450     public void setFillViewport(boolean fillViewport) {
451         if (fillViewport != mFillViewport) {
452             mFillViewport = fillViewport;
453             requestLayout();
454         }
455     }
456 
457     /**
458      * @return Whether arrow scrolling will animate its transition.
459      */
isSmoothScrollingEnabled()460     public boolean isSmoothScrollingEnabled() {
461         return mSmoothScrollingEnabled;
462     }
463 
464     /**
465      * Set whether arrow scrolling will animate its transition.
466      * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
467      */
setSmoothScrollingEnabled(boolean smoothScrollingEnabled)468     public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
469         mSmoothScrollingEnabled = smoothScrollingEnabled;
470     }
471 
472     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)473     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
474         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
475 
476         if (!mFillViewport) {
477             return;
478         }
479 
480         final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
481         if (heightMode == MeasureSpec.UNSPECIFIED) {
482             return;
483         }
484 
485         if (getChildCount() > 0) {
486             final View child = getChildAt(0);
487             final int widthPadding;
488             final int heightPadding;
489             final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
490             final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
491             if (targetSdkVersion >= VERSION_CODES.M) {
492                 widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
493                 heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
494             } else {
495                 widthPadding = mPaddingLeft + mPaddingRight;
496                 heightPadding = mPaddingTop + mPaddingBottom;
497             }
498 
499             final int desiredHeight = getMeasuredHeight() - heightPadding;
500             if (child.getMeasuredHeight() < desiredHeight) {
501                 final int childWidthMeasureSpec = getChildMeasureSpec(
502                         widthMeasureSpec, widthPadding, lp.width);
503                 final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
504                         desiredHeight, MeasureSpec.EXACTLY);
505                 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
506             }
507         }
508     }
509 
510     @Override
dispatchKeyEvent(KeyEvent event)511     public boolean dispatchKeyEvent(KeyEvent event) {
512         // Let the focused view and/or our descendants get the key first
513         return super.dispatchKeyEvent(event) || executeKeyEvent(event);
514     }
515 
516     /**
517      * You can call this function yourself to have the scroll view perform
518      * scrolling from a key event, just as if the event had been dispatched to
519      * it by the view hierarchy.
520      *
521      * @param event The key event to execute.
522      * @return Return true if the event was handled, else false.
523      */
executeKeyEvent(KeyEvent event)524     public boolean executeKeyEvent(KeyEvent event) {
525         mTempRect.setEmpty();
526 
527         if (!canScroll()) {
528             if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK
529                     && event.getKeyCode() != KeyEvent.KEYCODE_ESCAPE) {
530                 View currentFocused = findFocus();
531                 if (currentFocused == this) currentFocused = null;
532                 View nextFocused = FocusFinder.getInstance().findNextFocus(this,
533                         currentFocused, View.FOCUS_DOWN);
534                 return nextFocused != null
535                         && nextFocused != this
536                         && nextFocused.requestFocus(View.FOCUS_DOWN);
537             }
538             return false;
539         }
540 
541         boolean handled = false;
542         if (event.getAction() == KeyEvent.ACTION_DOWN) {
543             switch (event.getKeyCode()) {
544                 case KeyEvent.KEYCODE_DPAD_UP:
545                     if (!event.isAltPressed()) {
546                         handled = arrowScroll(View.FOCUS_UP);
547                     } else {
548                         handled = fullScroll(View.FOCUS_UP);
549                     }
550                     break;
551                 case KeyEvent.KEYCODE_DPAD_DOWN:
552                     if (!event.isAltPressed()) {
553                         handled = arrowScroll(View.FOCUS_DOWN);
554                     } else {
555                         handled = fullScroll(View.FOCUS_DOWN);
556                     }
557                     break;
558                 case KeyEvent.KEYCODE_MOVE_HOME:
559                     handled = fullScroll(View.FOCUS_UP);
560                     break;
561                 case KeyEvent.KEYCODE_MOVE_END:
562                     handled = fullScroll(View.FOCUS_DOWN);
563                     break;
564                 case KeyEvent.KEYCODE_PAGE_UP:
565                     handled = pageScroll(View.FOCUS_UP);
566                     break;
567                 case KeyEvent.KEYCODE_PAGE_DOWN:
568                     handled = pageScroll(View.FOCUS_DOWN);
569                     break;
570                 case KeyEvent.KEYCODE_SPACE:
571                     handled = pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
572                     break;
573             }
574         }
575 
576         return handled;
577     }
578 
inChild(int x, int y)579     private boolean inChild(int x, int y) {
580         if (getChildCount() > 0) {
581             final int scrollY = mScrollY;
582             final View child = getChildAt(0);
583             return !(y < child.getTop() - scrollY
584                     || y >= child.getBottom() - scrollY
585                     || x < child.getLeft()
586                     || x >= child.getRight());
587         }
588         return false;
589     }
590 
initOrResetVelocityTracker()591     private void initOrResetVelocityTracker() {
592         if (mVelocityTracker == null) {
593             mVelocityTracker = VelocityTracker.obtain();
594         } else {
595             mVelocityTracker.clear();
596         }
597     }
598 
initVelocityTrackerIfNotExists()599     private void initVelocityTrackerIfNotExists() {
600         if (mVelocityTracker == null) {
601             mVelocityTracker = VelocityTracker.obtain();
602         }
603     }
604 
initDifferentialFlingHelperIfNotExists()605     private void initDifferentialFlingHelperIfNotExists() {
606         if (mDifferentialMotionFlingHelper == null) {
607             mDifferentialMotionFlingHelper =
608                     new DifferentialMotionFlingHelper(
609                             mContext, new DifferentialFlingTarget());
610         }
611     }
612 
initHapticScrollFeedbackProviderIfNotExists()613     private void initHapticScrollFeedbackProviderIfNotExists() {
614         if (mHapticScrollFeedbackProvider == null) {
615             mHapticScrollFeedbackProvider = new HapticScrollFeedbackProvider(this);
616         }
617     }
618 
recycleVelocityTracker()619     private void recycleVelocityTracker() {
620         if (mVelocityTracker != null) {
621             mVelocityTracker.recycle();
622             mVelocityTracker = null;
623         }
624     }
625 
626     @Override
requestDisallowInterceptTouchEvent(boolean disallowIntercept)627     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
628         if (disallowIntercept) {
629             recycleVelocityTracker();
630         }
631         super.requestDisallowInterceptTouchEvent(disallowIntercept);
632     }
633 
634 
635     @Override
onInterceptTouchEvent(MotionEvent ev)636     public boolean onInterceptTouchEvent(MotionEvent ev) {
637         /*
638          * This method JUST determines whether we want to intercept the motion.
639          * If we return true, onMotionEvent will be called and we do the actual
640          * scrolling there.
641          */
642 
643         /*
644         * Shortcut the most recurring case: the user is in the dragging
645         * state and they is moving their finger.  We want to intercept this
646         * motion.
647         */
648         final int action = ev.getAction();
649         if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
650             return true;
651         }
652 
653         if (super.onInterceptTouchEvent(ev)) {
654             return true;
655         }
656 
657         /*
658          * Don't try to intercept touch if we can't scroll anyway.
659          */
660         if (getScrollY() == 0 && !canScrollVertically(1)) {
661             return false;
662         }
663 
664         switch (action & MotionEvent.ACTION_MASK) {
665             case MotionEvent.ACTION_MOVE: {
666                 /*
667                  * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
668                  * whether the user has moved far enough from their original down touch.
669                  */
670 
671                 /*
672                 * Locally do absolute value. mLastMotionY is set to the y value
673                 * of the down event.
674                 */
675                 final int activePointerId = mActivePointerId;
676                 if (activePointerId == INVALID_POINTER) {
677                     // If we don't have a valid id, the touch down wasn't on content.
678                     break;
679                 }
680 
681                 final int pointerIndex = ev.findPointerIndex(activePointerId);
682                 if (pointerIndex == -1) {
683                     Log.e(TAG, "Invalid pointerId=" + activePointerId
684                             + " in onInterceptTouchEvent");
685                     break;
686                 }
687 
688                 final int y = (int) ev.getY(pointerIndex);
689                 final int yDiff = Math.abs(y - mLastMotionY);
690                 if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
691                     mIsBeingDragged = true;
692                     mLastMotionY = y;
693                     initVelocityTrackerIfNotExists();
694                     mVelocityTracker.addMovement(ev);
695                     mNestedYOffset = 0;
696                     if (mScrollStrictSpan == null) {
697                         mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
698                     }
699                     final ViewParent parent = getParent();
700                     if (parent != null) {
701                         parent.requestDisallowInterceptTouchEvent(true);
702                     }
703                 }
704                 break;
705             }
706 
707             case MotionEvent.ACTION_DOWN: {
708                 final int y = (int) ev.getY();
709                 if (!inChild((int) ev.getX(), (int) y)) {
710                     mIsBeingDragged = false;
711                     recycleVelocityTracker();
712                     break;
713                 }
714 
715                 /*
716                  * Remember location of down touch.
717                  * ACTION_DOWN always refers to pointer index 0.
718                  */
719                 mLastMotionY = y;
720                 mActivePointerId = ev.getPointerId(0);
721 
722                 initOrResetVelocityTracker();
723                 mVelocityTracker.addMovement(ev);
724                 /*
725                  * If being flinged and user touches the screen, initiate drag;
726                  * otherwise don't. mScroller.isFinished should be false when
727                  * being flinged. We need to call computeScrollOffset() first so that
728                  * isFinished() is correct.
729                 */
730                 mScroller.computeScrollOffset();
731 
732                 // For variable refresh rate project to track the current velocity of this View
733                 if (viewVelocityApi()) {
734                     setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
735                 }
736 
737                 mIsBeingDragged = !mScroller.isFinished() || !mEdgeGlowBottom.isFinished()
738                     || !mEdgeGlowTop.isFinished();
739                 // Catch the edge effect if it is active.
740                 if (!mEdgeGlowTop.isFinished()) {
741                     mEdgeGlowTop.onPullDistance(0f, ev.getX() / getWidth());
742                 }
743                 if (!mEdgeGlowBottom.isFinished()) {
744                     mEdgeGlowBottom.onPullDistance(0f, 1f - ev.getX() / getWidth());
745                 }
746                 if (mIsBeingDragged && mScrollStrictSpan == null) {
747                     mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
748                 }
749                 startNestedScroll(SCROLL_AXIS_VERTICAL);
750                 break;
751             }
752 
753             case MotionEvent.ACTION_CANCEL:
754             case MotionEvent.ACTION_UP:
755                 /* Release the drag */
756                 mIsBeingDragged = false;
757                 mActivePointerId = INVALID_POINTER;
758                 recycleVelocityTracker();
759                 if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
760                     postInvalidateOnAnimation();
761                 }
762                 stopNestedScroll();
763                 break;
764             case MotionEvent.ACTION_POINTER_UP:
765                 onSecondaryPointerUp(ev);
766                 break;
767         }
768 
769         /*
770         * The only time we want to intercept motion events is if we are in the
771         * drag mode.
772         */
773         return mIsBeingDragged;
774     }
775 
shouldDisplayEdgeEffects()776     private boolean shouldDisplayEdgeEffects() {
777         return getOverScrollMode() != OVER_SCROLL_NEVER;
778     }
779 
780     @Override
onTouchEvent(MotionEvent ev)781     public boolean onTouchEvent(MotionEvent ev) {
782         initVelocityTrackerIfNotExists();
783 
784         MotionEvent vtev = MotionEvent.obtain(ev);
785 
786         final int actionMasked = ev.getActionMasked();
787 
788         if (actionMasked == MotionEvent.ACTION_DOWN) {
789             mNestedYOffset = 0;
790         }
791         vtev.offsetLocation(0, mNestedYOffset);
792 
793         switch (actionMasked) {
794             case MotionEvent.ACTION_DOWN: {
795                 if (getChildCount() == 0) {
796                     return false;
797                 }
798                 if (!mScroller.isFinished()) {
799                     final ViewParent parent = getParent();
800                     if (parent != null) {
801                         parent.requestDisallowInterceptTouchEvent(true);
802                     }
803                 }
804 
805                 /*
806                  * If being flinged and user touches, stop the fling. isFinished
807                  * will be false if being flinged.
808                  */
809                 if (!mScroller.isFinished()) {
810                     mScroller.abortAnimation();
811                     if (mFlingStrictSpan != null) {
812                         mFlingStrictSpan.finish();
813                         mFlingStrictSpan = null;
814                     }
815                 }
816 
817                 // Remember where the motion event started
818                 mLastMotionY = (int) ev.getY();
819                 mActivePointerId = ev.getPointerId(0);
820                 startNestedScroll(SCROLL_AXIS_VERTICAL);
821                 break;
822             }
823             case MotionEvent.ACTION_MOVE:
824                 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
825                 if (activePointerIndex == -1) {
826                     Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
827                     break;
828                 }
829 
830                 final int y = (int) ev.getY(activePointerIndex);
831                 int deltaY = mLastMotionY - y;
832                 if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
833                     deltaY -= mScrollConsumed[1];
834                     vtev.offsetLocation(0, mScrollOffset[1]);
835                     mNestedYOffset += mScrollOffset[1];
836                 }
837                 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
838                     final ViewParent parent = getParent();
839                     if (parent != null) {
840                         parent.requestDisallowInterceptTouchEvent(true);
841                     }
842                     mIsBeingDragged = true;
843                     if (deltaY > 0) {
844                         deltaY -= mTouchSlop;
845                     } else {
846                         deltaY += mTouchSlop;
847                     }
848                 }
849                 if (mIsBeingDragged) {
850                     // Scroll to follow the motion event
851                     mLastMotionY = y - mScrollOffset[1];
852 
853                     final int oldY = mScrollY;
854                     final int range = getScrollRange();
855                     final int overscrollMode = getOverScrollMode();
856                     boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
857                             (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
858 
859                     final float displacement = ev.getX(activePointerIndex) / getWidth();
860                     if (canOverscroll) {
861                         int consumed = 0;
862                         if (deltaY < 0 && mEdgeGlowBottom.getDistance() != 0f) {
863                             consumed = Math.round(getHeight()
864                                     * mEdgeGlowBottom.onPullDistance((float) deltaY / getHeight(),
865                                     1 - displacement));
866                         } else if (deltaY > 0 && mEdgeGlowTop.getDistance() != 0f) {
867                             consumed = Math.round(-getHeight()
868                                     * mEdgeGlowTop.onPullDistance((float) -deltaY / getHeight(),
869                                     displacement));
870                         }
871                         deltaY -= consumed;
872                     }
873 
874                     // Calling overScrollBy will call onOverScrolled, which
875                     // calls onScrollChanged if applicable.
876                     overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true);
877 
878                     final int scrolledDeltaY = mScrollY - oldY;
879                     final int unconsumedY = deltaY - scrolledDeltaY;
880                     if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
881                         mLastMotionY -= mScrollOffset[1];
882                         vtev.offsetLocation(0, mScrollOffset[1]);
883                         mNestedYOffset += mScrollOffset[1];
884                     } else if (canOverscroll && deltaY != 0f) {
885                         final int pulledToY = oldY + deltaY;
886                         if (pulledToY < 0) {
887                             mEdgeGlowTop.onPullDistance((float) -deltaY / getHeight(),
888                                     displacement);
889                             if (!mEdgeGlowBottom.isFinished()) {
890                                 mEdgeGlowBottom.onRelease();
891                             }
892                         } else if (pulledToY > range) {
893                             mEdgeGlowBottom.onPullDistance((float) deltaY / getHeight(),
894                                     1.f - displacement);
895                             if (!mEdgeGlowTop.isFinished()) {
896                                 mEdgeGlowTop.onRelease();
897                             }
898                         }
899                         if (shouldDisplayEdgeEffects()
900                                 && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
901                             postInvalidateOnAnimation();
902                         }
903                     }
904                 }
905                 break;
906             case MotionEvent.ACTION_UP:
907                 if (mIsBeingDragged) {
908                     final VelocityTracker velocityTracker = mVelocityTracker;
909                     velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
910                     int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
911 
912                     if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
913                         flingWithNestedDispatch(-initialVelocity);
914                     } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
915                             getScrollRange())) {
916                         postInvalidateOnAnimation();
917                     }
918 
919                     mActivePointerId = INVALID_POINTER;
920                     endDrag();
921                     velocityTracker.clear();
922                 }
923                 break;
924             case MotionEvent.ACTION_CANCEL:
925                 if (mIsBeingDragged && getChildCount() > 0) {
926                     if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
927                         postInvalidateOnAnimation();
928                     }
929                     mActivePointerId = INVALID_POINTER;
930                     endDrag();
931                 }
932                 break;
933             case MotionEvent.ACTION_POINTER_DOWN: {
934                 final int index = ev.getActionIndex();
935                 mLastMotionY = (int) ev.getY(index);
936                 mActivePointerId = ev.getPointerId(index);
937                 break;
938             }
939             case MotionEvent.ACTION_POINTER_UP:
940                 onSecondaryPointerUp(ev);
941                 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
942                 break;
943         }
944 
945         if (mVelocityTracker != null) {
946             mVelocityTracker.addMovement(vtev);
947         }
948         vtev.recycle();
949         return true;
950     }
951 
onSecondaryPointerUp(MotionEvent ev)952     private void onSecondaryPointerUp(MotionEvent ev) {
953         final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
954                 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
955         final int pointerId = ev.getPointerId(pointerIndex);
956         if (pointerId == mActivePointerId) {
957             // This was our active pointer going up. Choose a new
958             // active pointer and adjust accordingly.
959             // TODO: Make this decision more intelligent.
960             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
961             mLastMotionY = (int) ev.getY(newPointerIndex);
962             mActivePointerId = ev.getPointerId(newPointerIndex);
963             if (mVelocityTracker != null) {
964                 mVelocityTracker.clear();
965             }
966         }
967     }
968 
969     @Override
onGenericMotionEvent(MotionEvent event)970     public boolean onGenericMotionEvent(MotionEvent event) {
971         switch (event.getAction()) {
972             case MotionEvent.ACTION_SCROLL:
973                 final int axis;
974                 if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
975                     axis = MotionEvent.AXIS_VSCROLL;
976                 } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
977                     axis = MotionEvent.AXIS_SCROLL;
978                 } else {
979                     axis = -1;
980                 }
981 
982                 final float axisValue = (axis == -1) ? 0 : event.getAxisValue(axis);
983                 final int delta = Math.round(axisValue * mVerticalScrollFactor);
984                 if (delta != 0) {
985                     // Tracks whether or not we should attempt fling for this event.
986                     // Fling should not be attempted if the view is already at the limit of scroll,
987                     // since it conflicts with EdgeEffect.
988                     boolean hitLimit = false;
989                     final int range = getScrollRange();
990                     int oldScrollY = mScrollY;
991                     int newScrollY = oldScrollY - delta;
992 
993                     final int overscrollMode = getOverScrollMode();
994                     boolean canOverscroll = !event.isFromSource(InputDevice.SOURCE_MOUSE)
995                             && (overscrollMode == OVER_SCROLL_ALWAYS
996                             || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0));
997                     boolean absorbed = false;
998 
999                     if (newScrollY < 0) {
1000                         if (canOverscroll) {
1001                             mEdgeGlowTop.onPullDistance(-(float) newScrollY / getHeight(), 0.5f);
1002                             mEdgeGlowTop.onRelease();
1003                             invalidate();
1004                             absorbed = true;
1005                         }
1006                         newScrollY = 0;
1007                         hitLimit = true;
1008                     } else if (newScrollY > range) {
1009                         if (canOverscroll) {
1010                             mEdgeGlowBottom.onPullDistance(
1011                                     (float) (newScrollY - range) / getHeight(), 0.5f);
1012                             mEdgeGlowBottom.onRelease();
1013                             invalidate();
1014                             absorbed = true;
1015                         }
1016                         newScrollY = range;
1017                         hitLimit = true;
1018                     }
1019                     if (newScrollY != oldScrollY) {
1020                         super.scrollTo(mScrollX, newScrollY);
1021                         if (hitLimit) {
1022                             if (Flags.scrollFeedbackApi()) {
1023                                 initHapticScrollFeedbackProviderIfNotExists();
1024                                 mHapticScrollFeedbackProvider.onScrollLimit(
1025                                         event.getDeviceId(), event.getSource(), axis,
1026                                         /* isStart= */ newScrollY == 0);
1027                             }
1028                         } else {
1029                             if (Flags.scrollFeedbackApi()) {
1030                                 initHapticScrollFeedbackProviderIfNotExists();
1031                                 mHapticScrollFeedbackProvider.onScrollProgress(
1032                                         event.getDeviceId(), event.getSource(), axis, delta);
1033                             }
1034                             initDifferentialFlingHelperIfNotExists();
1035                             mDifferentialMotionFlingHelper.onMotionEvent(event, axis);
1036                         }
1037                         return true;
1038                     }
1039                     if (absorbed) {
1040                         return true;
1041                     }
1042                 }
1043                 break;
1044         }
1045 
1046         return super.onGenericMotionEvent(event);
1047     }
1048 
1049     @Override
onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)1050     protected void onOverScrolled(int scrollX, int scrollY,
1051             boolean clampedX, boolean clampedY) {
1052         // Treat animating scrolls differently; see #computeScroll() for why.
1053         if (!mScroller.isFinished()) {
1054             final int oldX = mScrollX;
1055             final int oldY = mScrollY;
1056             mScrollX = scrollX;
1057             mScrollY = scrollY;
1058             invalidateParentIfNeeded();
1059             onScrollChanged(mScrollX, mScrollY, oldX, oldY);
1060             if (clampedY) {
1061                 mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
1062             }
1063         } else {
1064             super.scrollTo(scrollX, scrollY);
1065         }
1066 
1067         awakenScrollBars();
1068     }
1069 
1070     /** @hide */
1071     @Override
performAccessibilityActionInternal(int action, Bundle arguments)1072     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
1073         if (super.performAccessibilityActionInternal(action, arguments)) {
1074             return true;
1075         }
1076         if (!isEnabled()) {
1077             return false;
1078         }
1079         switch (action) {
1080             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
1081             case R.id.accessibilityActionScrollDown: {
1082                 final int viewportHeight = getHeight() - mPaddingBottom - mPaddingTop;
1083                 final int targetScrollY = Math.min(mScrollY + viewportHeight, getScrollRange());
1084                 if (targetScrollY != mScrollY) {
1085                     smoothScrollTo(0, targetScrollY);
1086                     return true;
1087                 }
1088             } return false;
1089             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
1090             case R.id.accessibilityActionScrollUp: {
1091                 final int viewportHeight = getHeight() - mPaddingBottom - mPaddingTop;
1092                 final int targetScrollY = Math.max(mScrollY - viewportHeight, 0);
1093                 if (targetScrollY != mScrollY) {
1094                     smoothScrollTo(0, targetScrollY);
1095                     return true;
1096                 }
1097             } return false;
1098         }
1099         return false;
1100     }
1101 
1102     @Override
getAccessibilityClassName()1103     public CharSequence getAccessibilityClassName() {
1104         return ScrollView.class.getName();
1105     }
1106 
1107     /** @hide */
1108     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)1109     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1110         super.onInitializeAccessibilityNodeInfoInternal(info);
1111         if (isEnabled()) {
1112             final int scrollRange = getScrollRange();
1113             if (scrollRange > 0) {
1114                 info.setScrollable(true);
1115                 if (mScrollY > 0) {
1116                     info.addAction(
1117                             AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
1118                     info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP);
1119                 }
1120                 if (mScrollY < scrollRange) {
1121                     info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
1122                     info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN);
1123                 }
1124             }
1125         }
1126     }
1127 
1128     /** @hide */
1129     @Override
onInitializeAccessibilityEventInternal(AccessibilityEvent event)1130     public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
1131         super.onInitializeAccessibilityEventInternal(event);
1132         final boolean scrollable = getScrollRange() > 0;
1133         event.setScrollable(scrollable);
1134         event.setMaxScrollX(mScrollX);
1135         event.setMaxScrollY(getScrollRange());
1136     }
1137 
getScrollRange()1138     private int getScrollRange() {
1139         int scrollRange = 0;
1140         if (getChildCount() > 0) {
1141             View child = getChildAt(0);
1142             scrollRange = Math.max(0,
1143                     child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop));
1144         }
1145         return scrollRange;
1146     }
1147 
1148     /**
1149      * <p>
1150      * Finds the next focusable component that fits in the specified bounds.
1151      * </p>
1152      *
1153      * @param topFocus look for a candidate is the one at the top of the bounds
1154      *                 if topFocus is true, or at the bottom of the bounds if topFocus is
1155      *                 false
1156      * @param top      the top offset of the bounds in which a focusable must be
1157      *                 found
1158      * @param bottom   the bottom offset of the bounds in which a focusable must
1159      *                 be found
1160      * @return the next focusable component in the bounds or null if none can
1161      *         be found
1162      */
findFocusableViewInBounds(boolean topFocus, int top, int bottom)1163     private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) {
1164 
1165         List<View> focusables = getFocusables(View.FOCUS_FORWARD);
1166         View focusCandidate = null;
1167 
1168         /*
1169          * A fully contained focusable is one where its top is below the bound's
1170          * top, and its bottom is above the bound's bottom. A partially
1171          * contained focusable is one where some part of it is within the
1172          * bounds, but it also has some part that is not within bounds.  A fully contained
1173          * focusable is preferred to a partially contained focusable.
1174          */
1175         boolean foundFullyContainedFocusable = false;
1176 
1177         int count = focusables.size();
1178         for (int i = 0; i < count; i++) {
1179             View view = focusables.get(i);
1180             int viewTop = view.getTop();
1181             int viewBottom = view.getBottom();
1182 
1183             if (top < viewBottom && viewTop < bottom) {
1184                 /*
1185                  * the focusable is in the target area, it is a candidate for
1186                  * focusing
1187                  */
1188 
1189                 final boolean viewIsFullyContained = (top < viewTop) &&
1190                         (viewBottom < bottom);
1191 
1192                 if (focusCandidate == null) {
1193                     /* No candidate, take this one */
1194                     focusCandidate = view;
1195                     foundFullyContainedFocusable = viewIsFullyContained;
1196                 } else {
1197                     final boolean viewIsCloserToBoundary =
1198                             (topFocus && viewTop < focusCandidate.getTop()) ||
1199                                     (!topFocus && viewBottom > focusCandidate
1200                                             .getBottom());
1201 
1202                     if (foundFullyContainedFocusable) {
1203                         if (viewIsFullyContained && viewIsCloserToBoundary) {
1204                             /*
1205                              * We're dealing with only fully contained views, so
1206                              * it has to be closer to the boundary to beat our
1207                              * candidate
1208                              */
1209                             focusCandidate = view;
1210                         }
1211                     } else {
1212                         if (viewIsFullyContained) {
1213                             /* Any fully contained view beats a partially contained view */
1214                             focusCandidate = view;
1215                             foundFullyContainedFocusable = true;
1216                         } else if (viewIsCloserToBoundary) {
1217                             /*
1218                              * Partially contained view beats another partially
1219                              * contained view if it's closer
1220                              */
1221                             focusCandidate = view;
1222                         }
1223                     }
1224                 }
1225             }
1226         }
1227 
1228         return focusCandidate;
1229     }
1230 
1231     /**
1232      * <p>Handles scrolling in response to a "page up/down" shortcut press. This
1233      * method will scroll the view by one page up or down and give the focus
1234      * to the topmost/bottommost component in the new visible area. If no
1235      * component is a good candidate for focus, this scrollview reclaims the
1236      * focus.</p>
1237      *
1238      * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
1239      *                  to go one page up or
1240      *                  {@link android.view.View#FOCUS_DOWN} to go one page down
1241      * @return true if the key event is consumed by this method, false otherwise
1242      */
pageScroll(int direction)1243     public boolean pageScroll(int direction) {
1244         boolean down = direction == View.FOCUS_DOWN;
1245         int height = getHeight();
1246 
1247         if (down) {
1248             mTempRect.top = getScrollY() + height;
1249             int count = getChildCount();
1250             if (count > 0) {
1251                 View view = getChildAt(count - 1);
1252                 if (mTempRect.top + height > view.getBottom()) {
1253                     mTempRect.top = view.getBottom() - height;
1254                 }
1255             }
1256         } else {
1257             mTempRect.top = getScrollY() - height;
1258             if (mTempRect.top < 0) {
1259                 mTempRect.top = 0;
1260             }
1261         }
1262         mTempRect.bottom = mTempRect.top + height;
1263 
1264         return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
1265     }
1266 
1267     /**
1268      * <p>Handles scrolling in response to a "home/end" shortcut press. This
1269      * method will scroll the view to the top or bottom and give the focus
1270      * to the topmost/bottommost component in the new visible area. If no
1271      * component is a good candidate for focus, this scrollview reclaims the
1272      * focus.</p>
1273      *
1274      * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
1275      *                  to go the top of the view or
1276      *                  {@link android.view.View#FOCUS_DOWN} to go the bottom
1277      * @return true if the key event is consumed by this method, false otherwise
1278      */
fullScroll(int direction)1279     public boolean fullScroll(int direction) {
1280         boolean down = direction == View.FOCUS_DOWN;
1281         int height = getHeight();
1282 
1283         mTempRect.top = 0;
1284         mTempRect.bottom = height;
1285 
1286         if (down) {
1287             int count = getChildCount();
1288             if (count > 0) {
1289                 View view = getChildAt(count - 1);
1290                 mTempRect.bottom = view.getBottom() + mPaddingBottom;
1291                 mTempRect.top = mTempRect.bottom - height;
1292             }
1293         }
1294 
1295         return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
1296     }
1297 
1298     /**
1299      * <p>Scrolls the view to make the area defined by <code>top</code> and
1300      * <code>bottom</code> visible. This method attempts to give the focus
1301      * to a component visible in this area. If no component can be focused in
1302      * the new visible area, the focus is reclaimed by this ScrollView.</p>
1303      *
1304      * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
1305      *                  to go upward, {@link android.view.View#FOCUS_DOWN} to downward
1306      * @param top       the top offset of the new area to be made visible
1307      * @param bottom    the bottom offset of the new area to be made visible
1308      * @return true if the key event is consumed by this method, false otherwise
1309      */
scrollAndFocus(int direction, int top, int bottom)1310     private boolean scrollAndFocus(int direction, int top, int bottom) {
1311         boolean handled = true;
1312 
1313         int height = getHeight();
1314         int containerTop = getScrollY();
1315         int containerBottom = containerTop + height;
1316         boolean up = direction == View.FOCUS_UP;
1317 
1318         View newFocused = findFocusableViewInBounds(up, top, bottom);
1319         if (newFocused == null) {
1320             newFocused = this;
1321         }
1322 
1323         if (top >= containerTop && bottom <= containerBottom) {
1324             handled = false;
1325         } else {
1326             int delta = up ? (top - containerTop) : (bottom - containerBottom);
1327             doScrollY(delta);
1328         }
1329 
1330         if (newFocused != findFocus()) newFocused.requestFocus(direction);
1331 
1332         return handled;
1333     }
1334 
1335     /**
1336      * Handle scrolling in response to an up or down arrow click.
1337      *
1338      * @param direction The direction corresponding to the arrow key that was
1339      *                  pressed
1340      * @return True if we consumed the event, false otherwise
1341      */
arrowScroll(int direction)1342     public boolean arrowScroll(int direction) {
1343 
1344         View currentFocused = findFocus();
1345         if (currentFocused == this) currentFocused = null;
1346 
1347         View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
1348 
1349         final int maxJump = getMaxScrollAmount();
1350 
1351         if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) {
1352             nextFocused.getDrawingRect(mTempRect);
1353             offsetDescendantRectToMyCoords(nextFocused, mTempRect);
1354             int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1355             doScrollY(scrollDelta);
1356             nextFocused.requestFocus(direction);
1357         } else {
1358             // no new focus
1359             int scrollDelta = maxJump;
1360 
1361             if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
1362                 scrollDelta = getScrollY();
1363             } else if (direction == View.FOCUS_DOWN) {
1364                 if (getChildCount() > 0) {
1365                     int daBottom = getChildAt(0).getBottom();
1366                     int screenBottom = getScrollY() + getHeight() - mPaddingBottom;
1367                     if (daBottom - screenBottom < maxJump) {
1368                         scrollDelta = daBottom - screenBottom;
1369                     }
1370                 }
1371             }
1372             if (scrollDelta == 0) {
1373                 return false;
1374             }
1375             doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
1376         }
1377 
1378         if (currentFocused != null && currentFocused.isFocused()
1379                 && isOffScreen(currentFocused)) {
1380             // previously focused item still has focus and is off screen, give
1381             // it up (take it back to ourselves)
1382             // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
1383             // sure to
1384             // get it)
1385             final int descendantFocusability = getDescendantFocusability();  // save
1386             setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
1387             requestFocus();
1388             setDescendantFocusability(descendantFocusability);  // restore
1389         }
1390         return true;
1391     }
1392 
1393     /**
1394      * @return whether the descendant of this scroll view is scrolled off
1395      *  screen.
1396      */
isOffScreen(View descendant)1397     private boolean isOffScreen(View descendant) {
1398         return !isWithinDeltaOfScreen(descendant, 0, getHeight());
1399     }
1400 
1401     /**
1402      * @return whether the descendant of this scroll view is within delta
1403      *  pixels of being on the screen.
1404      */
isWithinDeltaOfScreen(View descendant, int delta, int height)1405     private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) {
1406         descendant.getDrawingRect(mTempRect);
1407         offsetDescendantRectToMyCoords(descendant, mTempRect);
1408 
1409         return (mTempRect.bottom + delta) >= getScrollY()
1410                 && (mTempRect.top - delta) <= (getScrollY() + height);
1411     }
1412 
1413     /**
1414      * Smooth scroll by a Y delta
1415      *
1416      * @param delta the number of pixels to scroll by on the Y axis
1417      */
doScrollY(int delta)1418     private void doScrollY(int delta) {
1419         if (delta != 0) {
1420             if (mSmoothScrollingEnabled) {
1421                 smoothScrollBy(0, delta);
1422             } else {
1423                 scrollBy(0, delta);
1424             }
1425         }
1426     }
1427 
1428     /**
1429      * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
1430      *
1431      * @param dx the number of pixels to scroll by on the X axis
1432      * @param dy the number of pixels to scroll by on the Y axis
1433      */
smoothScrollBy(int dx, int dy)1434     public final void smoothScrollBy(int dx, int dy) {
1435         if (getChildCount() == 0) {
1436             // Nothing to do.
1437             return;
1438         }
1439         long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
1440         if (duration > ANIMATED_SCROLL_GAP) {
1441             final int height = getHeight() - mPaddingBottom - mPaddingTop;
1442             final int bottom = getChildAt(0).getHeight();
1443             final int maxY = Math.max(0, bottom - height);
1444             final int scrollY = mScrollY;
1445             dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
1446 
1447             mScroller.startScroll(mScrollX, scrollY, 0, dy);
1448             postInvalidateOnAnimation();
1449         } else {
1450             if (!mScroller.isFinished()) {
1451                 mScroller.abortAnimation();
1452                 if (mFlingStrictSpan != null) {
1453                     mFlingStrictSpan.finish();
1454                     mFlingStrictSpan = null;
1455                 }
1456             }
1457             scrollBy(dx, dy);
1458         }
1459         mLastScroll = AnimationUtils.currentAnimationTimeMillis();
1460     }
1461 
1462     /**
1463      * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
1464      *
1465      * @param x the position where to scroll on the X axis
1466      * @param y the position where to scroll on the Y axis
1467      */
smoothScrollTo(int x, int y)1468     public final void smoothScrollTo(int x, int y) {
1469         smoothScrollBy(x - mScrollX, y - mScrollY);
1470     }
1471 
1472     /**
1473      * <p>The scroll range of a scroll view is the overall height of all of its
1474      * children.</p>
1475      */
1476     @Override
computeVerticalScrollRange()1477     protected int computeVerticalScrollRange() {
1478         final int count = getChildCount();
1479         final int contentHeight = getHeight() - mPaddingBottom - mPaddingTop;
1480         if (count == 0) {
1481             return contentHeight;
1482         }
1483 
1484         int scrollRange = getChildAt(0).getBottom();
1485         final int scrollY = mScrollY;
1486         final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
1487         if (scrollY < 0) {
1488             scrollRange -= scrollY;
1489         } else if (scrollY > overscrollBottom) {
1490             scrollRange += scrollY - overscrollBottom;
1491         }
1492 
1493         return scrollRange;
1494     }
1495 
1496     @Override
computeVerticalScrollOffset()1497     protected int computeVerticalScrollOffset() {
1498         return Math.max(0, super.computeVerticalScrollOffset());
1499     }
1500 
1501     @Override
measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)1502     protected void measureChild(View child, int parentWidthMeasureSpec,
1503             int parentHeightMeasureSpec) {
1504         ViewGroup.LayoutParams lp = child.getLayoutParams();
1505 
1506         int childWidthMeasureSpec;
1507         int childHeightMeasureSpec;
1508 
1509         childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
1510                 + mPaddingRight, lp.width);
1511         final int verticalPadding = mPaddingTop + mPaddingBottom;
1512         childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
1513                 Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding),
1514                 MeasureSpec.UNSPECIFIED);
1515 
1516         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1517     }
1518 
1519     @Override
measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)1520     protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
1521             int parentHeightMeasureSpec, int heightUsed) {
1522         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
1523 
1524         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
1525                 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
1526                         + widthUsed, lp.width);
1527         final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
1528                 heightUsed;
1529         final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
1530                 Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
1531                 MeasureSpec.UNSPECIFIED);
1532 
1533         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1534     }
1535 
1536     @Override
computeScroll()1537     public void computeScroll() {
1538         if (mScroller.computeScrollOffset()) {
1539             // This is called at drawing time by ViewGroup.  We don't want to
1540             // re-show the scrollbars at this point, which scrollTo will do,
1541             // so we replicate most of scrollTo here.
1542             //
1543             //         It's a little odd to call onScrollChanged from inside the drawing.
1544             //
1545             //         It is, except when you remember that computeScroll() is used to
1546             //         animate scrolling. So unless we want to defer the onScrollChanged()
1547             //         until the end of the animated scrolling, we don't really have a
1548             //         choice here.
1549             //
1550             //         I agree.  The alternative, which I think would be worse, is to post
1551             //         something and tell the subclasses later.  This is bad because there
1552             //         will be a window where mScrollX/Y is different from what the app
1553             //         thinks it is.
1554             //
1555             int oldX = mScrollX;
1556             int oldY = mScrollY;
1557             int x = mScroller.getCurrX();
1558             int y = mScroller.getCurrY();
1559             int deltaY = consumeFlingInStretch(y - oldY);
1560 
1561             if (oldX != x || deltaY != 0) {
1562                 final int range = getScrollRange();
1563                 final int overscrollMode = getOverScrollMode();
1564                 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
1565                         (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
1566 
1567                 overScrollBy(x - oldX, deltaY, oldX, oldY, 0, range,
1568                         0, mOverflingDistance, false);
1569                 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
1570 
1571                 if (canOverscroll && deltaY != 0) {
1572                     if (y < 0 && oldY >= 0) {
1573                         mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
1574                     } else if (y > range && oldY <= range) {
1575                         mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
1576                     }
1577                 }
1578             }
1579 
1580             if (!awakenScrollBars()) {
1581                 // Keep on drawing until the animation has finished.
1582                 postInvalidateOnAnimation();
1583             }
1584 
1585             // For variable refresh rate project to track the current velocity of this View
1586             if (viewVelocityApi()) {
1587                 setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
1588             }
1589         } else {
1590             if (mFlingStrictSpan != null) {
1591                 mFlingStrictSpan.finish();
1592                 mFlingStrictSpan = null;
1593             }
1594         }
1595     }
1596 
1597     /**
1598      * Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for
1599      * consuming deltas from EdgeEffects
1600      * @param unconsumed The unconsumed delta that the EdgeEffets may consume
1601      * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume.
1602      */
consumeFlingInStretch(int unconsumed)1603     private int consumeFlingInStretch(int unconsumed) {
1604         int scrollY = getScrollY();
1605         if (scrollY < 0 || scrollY > getScrollRange()) {
1606             // We've overscrolled, so don't stretch
1607             return unconsumed;
1608         }
1609         if (unconsumed > 0 && mEdgeGlowTop != null && mEdgeGlowTop.getDistance() != 0f) {
1610             int size = getHeight();
1611             float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size;
1612             int consumed = Math.round(-size / FLING_DESTRETCH_FACTOR
1613                     * mEdgeGlowTop.onPullDistance(deltaDistance, 0.5f));
1614             mEdgeGlowTop.onRelease();
1615             if (consumed != unconsumed) {
1616                 mEdgeGlowTop.finish();
1617             }
1618             return unconsumed - consumed;
1619         }
1620         if (unconsumed < 0 && mEdgeGlowBottom != null && mEdgeGlowBottom.getDistance() != 0f) {
1621             int size = getHeight();
1622             float deltaDistance = unconsumed * FLING_DESTRETCH_FACTOR / size;
1623             int consumed = Math.round(size / FLING_DESTRETCH_FACTOR
1624                     * mEdgeGlowBottom.onPullDistance(deltaDistance, 0.5f));
1625             mEdgeGlowBottom.onRelease();
1626             if (consumed != unconsumed) {
1627                 mEdgeGlowBottom.finish();
1628             }
1629             return unconsumed - consumed;
1630         }
1631         return unconsumed;
1632     }
1633 
1634     /**
1635      * Scrolls the view to the given child.
1636      *
1637      * @param child the View to scroll to
1638      */
scrollToDescendant(@onNull View child)1639     public void scrollToDescendant(@NonNull View child) {
1640         if (!mIsLayoutDirty) {
1641             child.getDrawingRect(mTempRect);
1642 
1643             /* Offset from child's local coordinates to ScrollView coordinates */
1644             offsetDescendantRectToMyCoords(child, mTempRect);
1645 
1646             int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1647 
1648             if (scrollDelta != 0) {
1649                 scrollBy(0, scrollDelta);
1650             }
1651         } else {
1652             mChildToScrollTo = child;
1653         }
1654     }
1655 
1656     /**
1657      * If rect is off screen, scroll just enough to get it (or at least the
1658      * first screen size chunk of it) on screen.
1659      *
1660      * @param rect      The rectangle.
1661      * @param immediate True to scroll immediately without animation
1662      * @return true if scrolling was performed
1663      */
scrollToChildRect(Rect rect, boolean immediate)1664     private boolean scrollToChildRect(Rect rect, boolean immediate) {
1665         final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
1666         final boolean scroll = delta != 0;
1667         if (scroll) {
1668             if (immediate) {
1669                 scrollBy(0, delta);
1670             } else {
1671                 smoothScrollBy(0, delta);
1672             }
1673         }
1674         return scroll;
1675     }
1676 
1677     /**
1678      * Compute the amount to scroll in the Y direction in order to get
1679      * a rectangle completely on the screen (or, if taller than the screen,
1680      * at least the first screen size chunk of it).
1681      *
1682      * @param rect The rect.
1683      * @return The scroll delta.
1684      */
computeScrollDeltaToGetChildRectOnScreen(Rect rect)1685     protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
1686         if (getChildCount() == 0) return 0;
1687 
1688         int height = getHeight();
1689         int screenTop = getScrollY();
1690         int screenBottom = screenTop + height;
1691 
1692         int fadingEdge = getVerticalFadingEdgeLength();
1693 
1694         // leave room for top fading edge as long as rect isn't at very top
1695         if (rect.top > 0) {
1696             screenTop += fadingEdge;
1697         }
1698 
1699         // leave room for bottom fading edge as long as rect isn't at very bottom
1700         if (rect.bottom < getChildAt(0).getHeight()) {
1701             screenBottom -= fadingEdge;
1702         }
1703 
1704         int scrollYDelta = 0;
1705 
1706         if (rect.bottom > screenBottom && rect.top > screenTop) {
1707             // need to move down to get it in view: move down just enough so
1708             // that the entire rectangle is in view (or at least the first
1709             // screen size chunk).
1710 
1711             if (rect.height() > height) {
1712                 // just enough to get screen size chunk on
1713                 scrollYDelta += (rect.top - screenTop);
1714             } else {
1715                 // get entire rect at bottom of screen
1716                 scrollYDelta += (rect.bottom - screenBottom);
1717             }
1718 
1719             // make sure we aren't scrolling beyond the end of our content
1720             int bottom = getChildAt(0).getBottom();
1721             int distanceToBottom = bottom - screenBottom;
1722             scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
1723 
1724         } else if (rect.top < screenTop && rect.bottom < screenBottom) {
1725             // need to move up to get it in view: move up just enough so that
1726             // entire rectangle is in view (or at least the first screen
1727             // size chunk of it).
1728 
1729             if (rect.height() > height) {
1730                 // screen size chunk
1731                 scrollYDelta -= (screenBottom - rect.bottom);
1732             } else {
1733                 // entire rect at top
1734                 scrollYDelta -= (screenTop - rect.top);
1735             }
1736 
1737             // make sure we aren't scrolling any further than the top our content
1738             scrollYDelta = Math.max(scrollYDelta, -getScrollY());
1739         }
1740         return scrollYDelta;
1741     }
1742 
1743     @Override
requestChildFocus(View child, View focused)1744     public void requestChildFocus(View child, View focused) {
1745         if (focused != null && focused.getRevealOnFocusHint()) {
1746             if (!mIsLayoutDirty) {
1747                 scrollToDescendant(focused);
1748             } else {
1749                 // The child may not be laid out yet, we can't compute the scroll yet
1750                 mChildToScrollTo = focused;
1751             }
1752         }
1753         super.requestChildFocus(child, focused);
1754     }
1755 
1756 
1757     /**
1758      * When looking for focus in children of a scroll view, need to be a little
1759      * more careful not to give focus to something that is scrolled off screen.
1760      *
1761      * This is more expensive than the default {@link android.view.ViewGroup}
1762      * implementation, otherwise this behavior might have been made the default.
1763      */
1764     @Override
onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)1765     protected boolean onRequestFocusInDescendants(int direction,
1766             Rect previouslyFocusedRect) {
1767 
1768         // convert from forward / backward notation to up / down / left / right
1769         // (ugh).
1770         if (direction == View.FOCUS_FORWARD) {
1771             direction = View.FOCUS_DOWN;
1772         } else if (direction == View.FOCUS_BACKWARD) {
1773             direction = View.FOCUS_UP;
1774         }
1775 
1776         final View nextFocus = previouslyFocusedRect == null ?
1777                 FocusFinder.getInstance().findNextFocus(this, null, direction) :
1778                 FocusFinder.getInstance().findNextFocusFromRect(this,
1779                         previouslyFocusedRect, direction);
1780 
1781         if (nextFocus == null) {
1782             return false;
1783         }
1784 
1785         if (isOffScreen(nextFocus)) {
1786             return false;
1787         }
1788 
1789         return nextFocus.requestFocus(direction, previouslyFocusedRect);
1790     }
1791 
1792     @Override
requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)1793     public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
1794             boolean immediate) {
1795         // offset into coordinate space of this scroll view
1796         rectangle.offset(child.getLeft() - child.getScrollX(),
1797                 child.getTop() - child.getScrollY());
1798 
1799         return scrollToChildRect(rectangle, immediate);
1800     }
1801 
1802     @Override
requestLayout()1803     public void requestLayout() {
1804         mIsLayoutDirty = true;
1805         super.requestLayout();
1806     }
1807 
1808     @Override
onDetachedFromWindow()1809     protected void onDetachedFromWindow() {
1810         super.onDetachedFromWindow();
1811 
1812         if (mScrollStrictSpan != null) {
1813             mScrollStrictSpan.finish();
1814             mScrollStrictSpan = null;
1815         }
1816         if (mFlingStrictSpan != null) {
1817             mFlingStrictSpan.finish();
1818             mFlingStrictSpan = null;
1819         }
1820     }
1821 
1822     @Override
onLayout(boolean changed, int l, int t, int r, int b)1823     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1824         super.onLayout(changed, l, t, r, b);
1825         mIsLayoutDirty = false;
1826         // Give a child focus if it needs it
1827         if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
1828             scrollToDescendant(mChildToScrollTo);
1829         }
1830         mChildToScrollTo = null;
1831 
1832         if (!isLaidOut()) {
1833             if (mSavedState != null) {
1834                 mScrollY = mSavedState.scrollPosition;
1835                 mSavedState = null;
1836             } // mScrollY default value is "0"
1837 
1838             final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0;
1839             final int scrollRange = Math.max(0,
1840                     childHeight - (b - t - mPaddingBottom - mPaddingTop));
1841 
1842             // Don't forget to clamp
1843             if (mScrollY > scrollRange) {
1844                 mScrollY = scrollRange;
1845             } else if (mScrollY < 0) {
1846                 mScrollY = 0;
1847             }
1848         }
1849 
1850         // Calling this with the present values causes it to re-claim them
1851         scrollTo(mScrollX, mScrollY);
1852     }
1853 
1854     @Override
onSizeChanged(int w, int h, int oldw, int oldh)1855     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1856         super.onSizeChanged(w, h, oldw, oldh);
1857 
1858         View currentFocused = findFocus();
1859         if (null == currentFocused || this == currentFocused)
1860             return;
1861 
1862         // If the currently-focused view was visible on the screen when the
1863         // screen was at the old height, then scroll the screen to make that
1864         // view visible with the new screen height.
1865         if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) {
1866             currentFocused.getDrawingRect(mTempRect);
1867             offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1868             int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1869             doScrollY(scrollDelta);
1870         }
1871     }
1872 
1873     /**
1874      * Return true if child is a descendant of parent, (or equal to the parent).
1875      */
isViewDescendantOf(View child, View parent)1876     private static boolean isViewDescendantOf(View child, View parent) {
1877         if (child == parent) {
1878             return true;
1879         }
1880 
1881         final ViewParent theParent = child.getParent();
1882         return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1883     }
1884 
1885     /**
1886      * Fling the scroll view
1887      *
1888      * @param velocityY The initial velocity in the Y direction. Positive
1889      *                  numbers mean that the finger/cursor is moving down the screen,
1890      *                  which means we want to scroll towards the top.
1891      */
fling(int velocityY)1892     public void fling(int velocityY) {
1893         if (getChildCount() > 0) {
1894             int height = getHeight() - mPaddingBottom - mPaddingTop;
1895             int bottom = getChildAt(0).getHeight();
1896 
1897             mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
1898                     Math.max(0, bottom - height), 0, height/2);
1899 
1900             // For variable refresh rate project to track the current velocity of this View
1901             if (viewVelocityApi()) {
1902                 setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
1903             }
1904             if (mFlingStrictSpan == null) {
1905                 mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
1906             }
1907 
1908             postInvalidateOnAnimation();
1909         }
1910     }
1911 
flingWithNestedDispatch(int velocityY)1912     private void flingWithNestedDispatch(int velocityY) {
1913         final boolean canFling = (mScrollY > 0 || velocityY > 0) &&
1914                 (mScrollY < getScrollRange() || velocityY < 0);
1915         if (!dispatchNestedPreFling(0, velocityY)) {
1916             final boolean consumed = dispatchNestedFling(0, velocityY, canFling);
1917             if (canFling) {
1918                 fling(velocityY);
1919             } else if (!consumed) {
1920                 if (!mEdgeGlowTop.isFinished()) {
1921                     if (shouldAbsorb(mEdgeGlowTop, -velocityY)) {
1922                         mEdgeGlowTop.onAbsorb(-velocityY);
1923                     } else {
1924                         fling(velocityY);
1925                     }
1926                 } else if (!mEdgeGlowBottom.isFinished()) {
1927                     if (shouldAbsorb(mEdgeGlowBottom, velocityY)) {
1928                         mEdgeGlowBottom.onAbsorb(velocityY);
1929                     } else {
1930                         fling(velocityY);
1931                     }
1932                 }
1933             }
1934         }
1935     }
1936 
1937     /**
1938      * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should
1939      * animate with a fling. It will animate with a fling if the velocity will remove the
1940      * EdgeEffect through its normal operation.
1941      *
1942      * @param edgeEffect The EdgeEffect that might absorb the velocity.
1943      * @param velocity The velocity of the fling motion
1944      * @return true if the velocity should be absorbed or false if it should be flung.
1945      */
shouldAbsorb(EdgeEffect edgeEffect, int velocity)1946     private boolean shouldAbsorb(EdgeEffect edgeEffect, int velocity) {
1947         if (velocity > 0) {
1948             return true;
1949         }
1950         float distance = edgeEffect.getDistance() * getHeight();
1951 
1952         // This is flinging without the spring, so let's see if it will fling past the overscroll
1953         float flingDistance = (float) mScroller.getSplineFlingDistance(-velocity);
1954 
1955         return flingDistance < distance;
1956     }
1957 
1958     @UnsupportedAppUsage
endDrag()1959     private void endDrag() {
1960         mIsBeingDragged = false;
1961 
1962         recycleVelocityTracker();
1963 
1964         if (shouldDisplayEdgeEffects()) {
1965             mEdgeGlowTop.onRelease();
1966             mEdgeGlowBottom.onRelease();
1967         }
1968 
1969         if (mScrollStrictSpan != null) {
1970             mScrollStrictSpan.finish();
1971             mScrollStrictSpan = null;
1972         }
1973     }
1974 
1975     /**
1976      * {@inheritDoc}
1977      *
1978      * <p>This version also clamps the scrolling to the bounds of our child.
1979      */
1980     @Override
scrollTo(int x, int y)1981     public void scrollTo(int x, int y) {
1982         // we rely on the fact the View.scrollBy calls scrollTo.
1983         if (getChildCount() > 0) {
1984             View child = getChildAt(0);
1985             x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
1986             y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
1987             if (x != mScrollX || y != mScrollY) {
1988                 super.scrollTo(x, y);
1989             }
1990         }
1991     }
1992 
1993     @Override
onStartNestedScroll(View child, View target, int nestedScrollAxes)1994     public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
1995         return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0;
1996     }
1997 
1998     @Override
onNestedScrollAccepted(View child, View target, int axes)1999     public void onNestedScrollAccepted(View child, View target, int axes) {
2000         super.onNestedScrollAccepted(child, target, axes);
2001         startNestedScroll(SCROLL_AXIS_VERTICAL);
2002     }
2003 
2004     /**
2005      * @inheritDoc
2006      */
2007     @Override
onStopNestedScroll(View target)2008     public void onStopNestedScroll(View target) {
2009         super.onStopNestedScroll(target);
2010     }
2011 
2012     @Override
onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)2013     public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
2014             int dxUnconsumed, int dyUnconsumed) {
2015         final int oldScrollY = mScrollY;
2016         scrollBy(0, dyUnconsumed);
2017         final int myConsumed = mScrollY - oldScrollY;
2018         final int myUnconsumed = dyUnconsumed - myConsumed;
2019         dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
2020     }
2021 
2022     /**
2023      * @inheritDoc
2024      */
2025     @Override
onNestedFling(View target, float velocityX, float velocityY, boolean consumed)2026     public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
2027         if (!consumed) {
2028             flingWithNestedDispatch((int) velocityY);
2029             return true;
2030         }
2031         return false;
2032     }
2033 
2034     @Override
draw(Canvas canvas)2035     public void draw(Canvas canvas) {
2036         super.draw(canvas);
2037         if (shouldDisplayEdgeEffects()) {
2038             final int scrollY = mScrollY;
2039             final boolean clipToPadding = getClipToPadding();
2040             if (!mEdgeGlowTop.isFinished()) {
2041                 final int restoreCount = canvas.save();
2042                 final int width;
2043                 final int height;
2044                 final float translateX;
2045                 final float translateY;
2046                 if (clipToPadding) {
2047                     width = getWidth() - mPaddingLeft - mPaddingRight;
2048                     height = getHeight() - mPaddingTop - mPaddingBottom;
2049                     translateX = mPaddingLeft;
2050                     translateY = mPaddingTop;
2051                 } else {
2052                     width = getWidth();
2053                     height = getHeight();
2054                     translateX = 0;
2055                     translateY = 0;
2056                 }
2057                 canvas.translate(translateX, Math.min(0, scrollY) + translateY);
2058                 mEdgeGlowTop.setSize(width, height);
2059                 if (mEdgeGlowTop.draw(canvas)) {
2060                     postInvalidateOnAnimation();
2061                 }
2062                 canvas.restoreToCount(restoreCount);
2063             }
2064             if (!mEdgeGlowBottom.isFinished()) {
2065                 final int restoreCount = canvas.save();
2066                 final int width;
2067                 final int height;
2068                 final float translateX;
2069                 final float translateY;
2070                 if (clipToPadding) {
2071                     width = getWidth() - mPaddingLeft - mPaddingRight;
2072                     height = getHeight() - mPaddingTop - mPaddingBottom;
2073                     translateX = mPaddingLeft;
2074                     translateY = mPaddingTop;
2075                 } else {
2076                     width = getWidth();
2077                     height = getHeight();
2078                     translateX = 0;
2079                     translateY = 0;
2080                 }
2081                 canvas.translate(-width + translateX,
2082                             Math.max(getScrollRange(), scrollY) + height + translateY);
2083                 canvas.rotate(180, width, 0);
2084                 mEdgeGlowBottom.setSize(width, height);
2085                 if (mEdgeGlowBottom.draw(canvas)) {
2086                     postInvalidateOnAnimation();
2087                 }
2088                 canvas.restoreToCount(restoreCount);
2089             }
2090         }
2091     }
2092 
clamp(int n, int my, int child)2093     private static int clamp(int n, int my, int child) {
2094         if (my >= child || n < 0) {
2095             /* my >= child is this case:
2096              *                    |--------------- me ---------------|
2097              *     |------ child ------|
2098              * or
2099              *     |--------------- me ---------------|
2100              *            |------ child ------|
2101              * or
2102              *     |--------------- me ---------------|
2103              *                                  |------ child ------|
2104              *
2105              * n < 0 is this case:
2106              *     |------ me ------|
2107              *                    |-------- child --------|
2108              *     |-- mScrollX --|
2109              */
2110             return 0;
2111         }
2112         if ((my+n) > child) {
2113             /* this case:
2114              *                    |------ me ------|
2115              *     |------ child ------|
2116              *     |-- mScrollX --|
2117              */
2118             return child-my;
2119         }
2120         return n;
2121     }
2122 
2123     @Override
onRestoreInstanceState(Parcelable state)2124     protected void onRestoreInstanceState(Parcelable state) {
2125         if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
2126             // Some old apps reused IDs in ways they shouldn't have.
2127             // Don't break them, but they don't get scroll state restoration.
2128             super.onRestoreInstanceState(state);
2129             return;
2130         }
2131         SavedState ss = (SavedState) state;
2132         super.onRestoreInstanceState(ss.getSuperState());
2133         mSavedState = ss;
2134         requestLayout();
2135     }
2136 
2137     @Override
onSaveInstanceState()2138     protected Parcelable onSaveInstanceState() {
2139         if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
2140             // Some old apps reused IDs in ways they shouldn't have.
2141             // Don't break them, but they don't get scroll state restoration.
2142             return super.onSaveInstanceState();
2143         }
2144         Parcelable superState = super.onSaveInstanceState();
2145         SavedState ss = new SavedState(superState);
2146         ss.scrollPosition = mScrollY;
2147         return ss;
2148     }
2149 
2150     /** @hide */
2151     @Override
encodeProperties(@onNull ViewHierarchyEncoder encoder)2152     protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
2153         super.encodeProperties(encoder);
2154         encoder.addProperty("fillViewport", mFillViewport);
2155     }
2156 
2157     static class SavedState extends BaseSavedState {
2158         public int scrollPosition;
2159 
SavedState(Parcelable superState)2160         SavedState(Parcelable superState) {
2161             super(superState);
2162         }
2163 
SavedState(Parcel source)2164         public SavedState(Parcel source) {
2165             super(source);
2166             scrollPosition = source.readInt();
2167         }
2168 
2169         @Override
writeToParcel(Parcel dest, int flags)2170         public void writeToParcel(Parcel dest, int flags) {
2171             super.writeToParcel(dest, flags);
2172             dest.writeInt(scrollPosition);
2173         }
2174 
2175         @Override
toString()2176         public String toString() {
2177             return "ScrollView.SavedState{"
2178                     + Integer.toHexString(System.identityHashCode(this))
2179                     + " scrollPosition=" + scrollPosition + "}";
2180         }
2181 
2182         public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR
2183                 = new Parcelable.Creator<SavedState>() {
2184             public SavedState createFromParcel(Parcel in) {
2185                 return new SavedState(in);
2186             }
2187 
2188             public SavedState[] newArray(int size) {
2189                 return new SavedState[size];
2190             }
2191         };
2192     }
2193 
2194     private class DifferentialFlingTarget
2195             implements DifferentialMotionFlingHelper.DifferentialMotionFlingTarget {
2196         @Override
startDifferentialMotionFling(float velocity)2197         public boolean startDifferentialMotionFling(float velocity) {
2198             stopDifferentialMotionFling();
2199             fling((int) velocity);
2200             return true;
2201         }
2202 
2203         @Override
stopDifferentialMotionFling()2204         public void stopDifferentialMotionFling() {
2205             mScroller.abortAnimation();
2206         }
2207 
2208         @Override
getScaledScrollFactor()2209         public float getScaledScrollFactor() {
2210             return -mVerticalScrollFactor;
2211         }
2212     }
2213 }
2214