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