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