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