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