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