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