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