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