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