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