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