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