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