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 android.support.v4.widget;
19
20import static android.support.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.Bundle;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.support.annotation.RestrictTo;
30import android.support.v4.view.AccessibilityDelegateCompat;
31import android.support.v4.view.InputDeviceCompat;
32import android.support.v4.view.NestedScrollingChild2;
33import android.support.v4.view.NestedScrollingChildHelper;
34import android.support.v4.view.NestedScrollingParent;
35import android.support.v4.view.NestedScrollingParentHelper;
36import android.support.v4.view.ScrollingView;
37import android.support.v4.view.ViewCompat;
38import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
39import android.support.v4.view.accessibility.AccessibilityRecordCompat;
40import android.util.AttributeSet;
41import android.util.Log;
42import android.util.TypedValue;
43import android.view.FocusFinder;
44import android.view.KeyEvent;
45import android.view.MotionEvent;
46import android.view.VelocityTracker;
47import android.view.View;
48import android.view.ViewConfiguration;
49import android.view.ViewGroup;
50import android.view.ViewParent;
51import android.view.accessibility.AccessibilityEvent;
52import android.view.animation.AnimationUtils;
53import android.widget.EdgeEffect;
54import android.widget.FrameLayout;
55import android.widget.OverScroller;
56import android.widget.ScrollView;
57
58import 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 */
65public 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         */
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
184    public NestedScrollView(Context context) {
185        this(context, null);
186    }
187
188    public NestedScrollView(Context context, AttributeSet attrs) {
189        this(context, attrs, 0);
190    }
191
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
215    public void setNestedScrollingEnabled(boolean enabled) {
216        mChildHelper.setNestedScrollingEnabled(enabled);
217    }
218
219    @Override
220    public boolean isNestedScrollingEnabled() {
221        return mChildHelper.isNestedScrollingEnabled();
222    }
223
224    @Override
225    public boolean startNestedScroll(int axes) {
226        return mChildHelper.startNestedScroll(axes);
227    }
228
229    @Override
230    public boolean startNestedScroll(int axes, int type) {
231        return mChildHelper.startNestedScroll(axes, type);
232    }
233
234    @Override
235    public void stopNestedScroll() {
236        mChildHelper.stopNestedScroll();
237    }
238
239    @Override
240    public void stopNestedScroll(int type) {
241        mChildHelper.stopNestedScroll(type);
242    }
243
244    @Override
245    public boolean hasNestedScrollingParent() {
246        return mChildHelper.hasNestedScrollingParent();
247    }
248
249    @Override
250    public boolean hasNestedScrollingParent(int type) {
251        return mChildHelper.hasNestedScrollingParent(type);
252    }
253
254    @Override
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
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
269    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
270        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
271    }
272
273    @Override
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
280    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
281        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
282    }
283
284    @Override
285    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
286        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
287    }
288
289    // NestedScrollingParent
290
291    @Override
292    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
293        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
294    }
295
296    @Override
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
303    public void onStopNestedScroll(View target) {
304        mParentHelper.onStopNestedScroll(target);
305        stopNestedScroll();
306    }
307
308    @Override
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
319    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
320        dispatchNestedPreScroll(dx, dy, consumed, null);
321    }
322
323    @Override
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
333    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
334        return dispatchNestedPreFling(velocityX, velocityY);
335    }
336
337    @Override
338    public int getNestedScrollAxes() {
339        return mParentHelper.getNestedScrollAxes();
340    }
341
342    // ScrollView import
343
344    @Override
345    public boolean shouldDelayChildPressedState() {
346        return true;
347    }
348
349    @Override
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
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     */
384    public int getMaxScrollAmount() {
385        return (int) (MAX_SCROLL_FACTOR * getHeight());
386    }
387
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
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
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
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
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     */
444    public void setOnScrollChangeListener(OnScrollChangeListener l) {
445        mOnScrollChangeListener = l;
446    }
447
448    /**
449     * @return Returns true this ScrollView can be scrolled
450     */
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     */
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     */
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     */
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     */
497    public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
498        mSmoothScrollingEnabled = smoothScrollingEnabled;
499    }
500
501    @Override
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
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
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     */
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
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
609    private void initOrResetVelocityTracker() {
610        if (mVelocityTracker == null) {
611            mVelocityTracker = VelocityTracker.obtain();
612        } else {
613            mVelocityTracker.clear();
614        }
615    }
616
617    private void initVelocityTrackerIfNotExists() {
618        if (mVelocityTracker == null) {
619            mVelocityTracker = VelocityTracker.obtain();
620        }
621    }
622
623    private void recycleVelocityTracker() {
624        if (mVelocityTracker != null) {
625            mVelocityTracker.recycle();
626            mVelocityTracker = null;
627        }
628    }
629
630    @Override
631    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
632        if (disallowIntercept) {
633            recycleVelocityTracker();
634        }
635        super.requestDisallowInterceptTouchEvent(disallowIntercept);
636    }
637
638    @Override
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
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
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
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
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
967    protected void onOverScrolled(int scrollX, int scrollY,
968            boolean clampedX, boolean clampedY) {
969        super.scrollTo(scrollX, scrollY);
970    }
971
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
1387    public int computeVerticalScrollOffset() {
1388        return Math.max(0, super.computeVerticalScrollOffset());
1389    }
1390
1391    /** @hide */
1392    @RestrictTo(LIBRARY_GROUP)
1393    @Override
1394    public int computeVerticalScrollExtent() {
1395        return super.computeVerticalScrollExtent();
1396    }
1397
1398    /** @hide */
1399    @RestrictTo(LIBRARY_GROUP)
1400    @Override
1401    public int computeHorizontalScrollRange() {
1402        return super.computeHorizontalScrollRange();
1403    }
1404
1405    /** @hide */
1406    @RestrictTo(LIBRARY_GROUP)
1407    @Override
1408    public int computeHorizontalScrollOffset() {
1409        return super.computeHorizontalScrollOffset();
1410    }
1411
1412    /** @hide */
1413    @RestrictTo(LIBRARY_GROUP)
1414    @Override
1415    public int computeHorizontalScrollExtent() {
1416        return super.computeHorizontalScrollExtent();
1417    }
1418
1419    @Override
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
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
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     */
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     */
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     */
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
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
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
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
1663    public void requestLayout() {
1664        mIsLayoutDirty = true;
1665        super.requestLayout();
1666    }
1667
1668    @Override
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
1702    public void onAttachedToWindow() {
1703        super.onAttachedToWindow();
1704
1705        mIsLaidOut = false;
1706    }
1707
1708    @Override
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     */
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     */
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
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
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
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
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
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
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
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
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
1900        SavedState(Parcelable superState) {
1901            super(superState);
1902        }
1903
1904        SavedState(Parcel source) {
1905            super(source);
1906            scrollPosition = source.readInt();
1907        }
1908
1909        @Override
1910        public void writeToParcel(Parcel dest, int flags) {
1911            super.writeToParcel(dest, flags);
1912            dest.writeInt(scrollPosition);
1913        }
1914
1915        @Override
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
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
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
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