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