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
18package androidx.core.widget;
19
20import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
21
22import android.content.Context;
23import android.content.res.TypedArray;
24import android.graphics.Canvas;
25import android.graphics.Rect;
26import android.os.Build;
27import android.os.Bundle;
28import android.os.Parcel;
29import android.os.Parcelable;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.util.TypedValue;
33import android.view.FocusFinder;
34import android.view.KeyEvent;
35import android.view.MotionEvent;
36import android.view.VelocityTracker;
37import android.view.View;
38import android.view.ViewConfiguration;
39import android.view.ViewGroup;
40import android.view.ViewParent;
41import android.view.accessibility.AccessibilityEvent;
42import android.view.animation.AnimationUtils;
43import android.widget.EdgeEffect;
44import android.widget.FrameLayout;
45import android.widget.OverScroller;
46import android.widget.ScrollView;
47
48import androidx.annotation.NonNull;
49import androidx.annotation.Nullable;
50import androidx.annotation.RestrictTo;
51import androidx.core.view.AccessibilityDelegateCompat;
52import androidx.core.view.InputDeviceCompat;
53import androidx.core.view.NestedScrollingChild2;
54import androidx.core.view.NestedScrollingChildHelper;
55import androidx.core.view.NestedScrollingParent2;
56import androidx.core.view.NestedScrollingParentHelper;
57import androidx.core.view.ScrollingView;
58import androidx.core.view.ViewCompat;
59import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
60import androidx.core.view.accessibility.AccessibilityRecordCompat;
61
62import 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 */
69public 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         */
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
188    public NestedScrollView(@NonNull Context context) {
189        this(context, null);
190    }
191
192    public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
193        this(context, attrs, 0);
194    }
195
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
220    public boolean startNestedScroll(int axes, int type) {
221        return mChildHelper.startNestedScroll(axes, type);
222    }
223
224    @Override
225    public void stopNestedScroll(int type) {
226        mChildHelper.stopNestedScroll(type);
227    }
228
229    @Override
230    public boolean hasNestedScrollingParent(int type) {
231        return mChildHelper.hasNestedScrollingParent(type);
232    }
233
234    @Override
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
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
250    public void setNestedScrollingEnabled(boolean enabled) {
251        mChildHelper.setNestedScrollingEnabled(enabled);
252    }
253
254    @Override
255    public boolean isNestedScrollingEnabled() {
256        return mChildHelper.isNestedScrollingEnabled();
257    }
258
259    @Override
260    public boolean startNestedScroll(int axes) {
261        return startNestedScroll(axes, ViewCompat.TYPE_TOUCH);
262    }
263
264    @Override
265    public void stopNestedScroll() {
266        stopNestedScroll(ViewCompat.TYPE_TOUCH);
267    }
268
269    @Override
270    public boolean hasNestedScrollingParent() {
271        return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);
272    }
273
274    @Override
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
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
287    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
288        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
289    }
290
291    @Override
292    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
293        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
294    }
295
296    // NestedScrollingParent2
297
298    @Override
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
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
312    public void onStopNestedScroll(@NonNull View target, int type) {
313        mParentHelper.onStopNestedScroll(target, type);
314        stopNestedScroll(type);
315    }
316
317    @Override
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
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
337    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
338        return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
339    }
340
341    @Override
342    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
343        onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
344    }
345
346    @Override
347    public void onStopNestedScroll(View target) {
348        onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
349    }
350
351    @Override
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
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
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
373    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
374        return dispatchNestedPreFling(velocityX, velocityY);
375    }
376
377    @Override
378    public int getNestedScrollAxes() {
379        return mParentHelper.getNestedScrollAxes();
380    }
381
382    // ScrollView import
383
384    @Override
385    public boolean shouldDelayChildPressedState() {
386        return true;
387    }
388
389    @Override
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
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     */
426    public int getMaxScrollAmount() {
427        return (int) (MAX_SCROLL_FACTOR * getHeight());
428    }
429
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
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
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
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
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     */
486    public void setOnScrollChangeListener(@Nullable OnScrollChangeListener l) {
487        mOnScrollChangeListener = l;
488    }
489
490    /**
491     * @return Returns true this ScrollView can be scrolled
492     */
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     */
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     */
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     */
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     */
541    public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
542        mSmoothScrollingEnabled = smoothScrollingEnabled;
543    }
544
545    @Override
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
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
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     */
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
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
657    private void initOrResetVelocityTracker() {
658        if (mVelocityTracker == null) {
659            mVelocityTracker = VelocityTracker.obtain();
660        } else {
661            mVelocityTracker.clear();
662        }
663    }
664
665    private void initVelocityTrackerIfNotExists() {
666        if (mVelocityTracker == null) {
667            mVelocityTracker = VelocityTracker.obtain();
668        }
669    }
670
671    private void recycleVelocityTracker() {
672        if (mVelocityTracker != null) {
673            mVelocityTracker.recycle();
674            mVelocityTracker = null;
675        }
676    }
677
678    @Override
679    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
680        if (disallowIntercept) {
681            recycleVelocityTracker();
682        }
683        super.requestDisallowInterceptTouchEvent(disallowIntercept);
684    }
685
686    @Override
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
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
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
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
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
1015    protected void onOverScrolled(int scrollX, int scrollY,
1016            boolean clampedX, boolean clampedY) {
1017        super.scrollTo(scrollX, scrollY);
1018    }
1019
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
1443    public int computeVerticalScrollOffset() {
1444        return Math.max(0, super.computeVerticalScrollOffset());
1445    }
1446
1447    /** @hide */
1448    @RestrictTo(LIBRARY_GROUP)
1449    @Override
1450    public int computeVerticalScrollExtent() {
1451        return super.computeVerticalScrollExtent();
1452    }
1453
1454    /** @hide */
1455    @RestrictTo(LIBRARY_GROUP)
1456    @Override
1457    public int computeHorizontalScrollRange() {
1458        return super.computeHorizontalScrollRange();
1459    }
1460
1461    /** @hide */
1462    @RestrictTo(LIBRARY_GROUP)
1463    @Override
1464    public int computeHorizontalScrollOffset() {
1465        return super.computeHorizontalScrollOffset();
1466    }
1467
1468    /** @hide */
1469    @RestrictTo(LIBRARY_GROUP)
1470    @Override
1471    public int computeHorizontalScrollExtent() {
1472        return super.computeHorizontalScrollExtent();
1473    }
1474
1475    @Override
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
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
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     */
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     */
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     */
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
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
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
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
1726    public void requestLayout() {
1727        mIsLayoutDirty = true;
1728        super.requestLayout();
1729    }
1730
1731    @Override
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
1770    public void onAttachedToWindow() {
1771        super.onAttachedToWindow();
1772
1773        mIsLaidOut = false;
1774    }
1775
1776    @Override
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     */
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     */
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
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
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
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
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
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
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
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
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
1991        SavedState(Parcelable superState) {
1992            super(superState);
1993        }
1994
1995        SavedState(Parcel source) {
1996            super(source);
1997            scrollPosition = source.readInt();
1998        }
1999
2000        @Override
2001        public void writeToParcel(Parcel dest, int flags) {
2002            super.writeToParcel(dest, flags);
2003            dest.writeInt(scrollPosition);
2004        }
2005
2006        @Override
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
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
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
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