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