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 com.android.browser.view;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.PointF;
22import android.graphics.Rect;
23import android.os.StrictMode;
24import android.util.AttributeSet;
25import android.view.FocusFinder;
26import android.view.InputDevice;
27import android.view.KeyEvent;
28import android.view.MotionEvent;
29import android.view.VelocityTracker;
30import android.view.View;
31import android.view.ViewConfiguration;
32import android.view.ViewDebug;
33import android.view.ViewGroup;
34import android.view.ViewParent;
35import android.view.accessibility.AccessibilityEvent;
36import android.view.accessibility.AccessibilityNodeInfo;
37import android.view.animation.AnimationUtils;
38import android.widget.FrameLayout;
39import android.widget.LinearLayout;
40import android.widget.OverScroller;
41import android.widget.TextView;
42
43import com.android.internal.R;
44
45import java.util.List;
46
47/**
48 * Layout container for a view hierarchy that can be scrolled by the user,
49 * allowing it to be larger than the physical display.  A ScrollView
50 * is a {@link FrameLayout}, meaning you should place one child in it
51 * containing the entire contents to scroll; this child may itself be a layout
52 * manager with a complex hierarchy of objects.  A child that is often used
53 * is a {@link LinearLayout} in a vertical orientation, presenting a vertical
54 * array of top-level items that the user can scroll through.
55 *
56 * <p>The {@link TextView} class also
57 * takes care of its own scrolling, so does not require a ScrollView, but
58 * using the two together is possible to achieve the effect of a text view
59 * within a larger container.
60 *
61 * <p>ScrollView only supports vertical scrolling.
62 *
63 * @attr ref android.R.styleable#ScrollView_fillViewport
64 */
65public class ScrollerView extends FrameLayout {
66    static final int ANIMATED_SCROLL_GAP = 250;
67
68    static final float MAX_SCROLL_FACTOR = 0.5f;
69
70    private long mLastScroll;
71
72    private final Rect mTempRect = new Rect();
73    protected OverScroller mScroller;
74
75    /**
76     * Position of the last motion event.
77     */
78    private float mLastMotionY;
79
80    /**
81     * True when the layout has changed but the traversal has not come through yet.
82     * Ideally the view hierarchy would keep track of this for us.
83     */
84    private boolean mIsLayoutDirty = true;
85
86    /**
87     * The child to give focus to in the event that a child has requested focus while the
88     * layout is dirty. This prevents the scroll from being wrong if the child has not been
89     * laid out before requesting focus.
90     */
91    protected View mChildToScrollTo = null;
92
93    /**
94     * True if the user is currently dragging this ScrollView around. This is
95     * not the same as 'is being flinged', which can be checked by
96     * mScroller.isFinished() (flinging begins when the user lifts his finger).
97     */
98    protected boolean mIsBeingDragged = false;
99
100    /**
101     * Determines speed during touch scrolling
102     */
103    private VelocityTracker mVelocityTracker;
104
105    /**
106     * When set to true, the scroll view measure its child to make it fill the currently
107     * visible area.
108     */
109    @ViewDebug.ExportedProperty(category = "layout")
110    private boolean mFillViewport;
111
112    /**
113     * Whether arrow scrolling is animated.
114     */
115    private boolean mSmoothScrollingEnabled = true;
116
117    private int mTouchSlop;
118    protected int mMinimumVelocity;
119    private int mMaximumVelocity;
120
121    private int mOverscrollDistance;
122    private int mOverflingDistance;
123
124    /**
125     * ID of the active pointer. This is used to retain consistency during
126     * drags/flings if multiple pointers are used.
127     */
128    private int mActivePointerId = INVALID_POINTER;
129
130    /**
131     * The StrictMode "critical time span" objects to catch animation
132     * stutters.  Non-null when a time-sensitive animation is
133     * in-flight.  Must call finish() on them when done animating.
134     * These are no-ops on user builds.
135     */
136    private StrictMode.Span mScrollStrictSpan = null;  // aka "drag"
137    private StrictMode.Span mFlingStrictSpan = null;
138
139    /**
140     * Sentinel value for no current active pointer.
141     * Used by {@link #mActivePointerId}.
142     */
143    private static final int INVALID_POINTER = -1;
144
145    /**
146     * orientation of the scrollview
147     */
148    protected boolean mHorizontal;
149
150    protected boolean mIsOrthoDragged;
151    private float mLastOrthoCoord;
152    private View mDownView;
153    private PointF mDownCoords;
154
155
156    public ScrollerView(Context context) {
157        this(context, null);
158    }
159
160    public ScrollerView(Context context, AttributeSet attrs) {
161        this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
162    }
163
164    public ScrollerView(Context context, AttributeSet attrs, int defStyle) {
165        super(context, attrs, defStyle);
166        initScrollView();
167
168        TypedArray a =
169            context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ScrollView, defStyle, 0);
170
171        setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
172
173        a.recycle();
174    }
175
176    private void initScrollView() {
177        mScroller = new OverScroller(getContext());
178        setFocusable(true);
179        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
180        setWillNotDraw(false);
181        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
182        mTouchSlop = configuration.getScaledTouchSlop();
183        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
184        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
185        mOverscrollDistance = configuration.getScaledOverscrollDistance();
186        mOverflingDistance = configuration.getScaledOverflingDistance();
187        mDownCoords = new PointF();
188    }
189
190    public void setOrientation(int orientation) {
191        mHorizontal = (orientation == LinearLayout.HORIZONTAL);
192        requestLayout();
193    }
194
195    @Override
196    public boolean shouldDelayChildPressedState() {
197        return true;
198    }
199
200    @Override
201    protected float getTopFadingEdgeStrength() {
202        if (getChildCount() == 0) {
203            return 0.0f;
204        }
205        if (mHorizontal) {
206            final int length = getHorizontalFadingEdgeLength();
207            if (mScrollX < length) {
208                return mScrollX / (float) length;
209            }
210        } else {
211            final int length = getVerticalFadingEdgeLength();
212            if (mScrollY < length) {
213                return mScrollY / (float) length;
214            }
215        }
216        return 1.0f;
217    }
218
219    @Override
220    protected float getBottomFadingEdgeStrength() {
221        if (getChildCount() == 0) {
222            return 0.0f;
223        }
224        if (mHorizontal) {
225            final int length = getHorizontalFadingEdgeLength();
226            final int bottomEdge = getWidth() - mPaddingRight;
227            final int span = getChildAt(0).getRight() - mScrollX - bottomEdge;
228            if (span < length) {
229                return span / (float) length;
230            }
231        } else {
232            final int length = getVerticalFadingEdgeLength();
233            final int bottomEdge = getHeight() - mPaddingBottom;
234            final int span = getChildAt(0).getBottom() - mScrollY - bottomEdge;
235            if (span < length) {
236                return span / (float) length;
237            }
238        }
239        return 1.0f;
240    }
241
242    /**
243     * @return The maximum amount this scroll view will scroll in response to
244     *   an arrow event.
245     */
246    public int getMaxScrollAmount() {
247        return (int) (MAX_SCROLL_FACTOR * (mHorizontal
248                ? (mRight - mLeft) : (mBottom - mTop)));
249    }
250
251
252    @Override
253    public void addView(View child) {
254        if (getChildCount() > 0) {
255            throw new IllegalStateException("ScrollView can host only one direct child");
256        }
257
258        super.addView(child);
259    }
260
261    @Override
262    public void addView(View child, int index) {
263        if (getChildCount() > 0) {
264            throw new IllegalStateException("ScrollView can host only one direct child");
265        }
266
267        super.addView(child, index);
268    }
269
270    @Override
271    public void addView(View child, ViewGroup.LayoutParams params) {
272        if (getChildCount() > 0) {
273            throw new IllegalStateException("ScrollView can host only one direct child");
274        }
275
276        super.addView(child, params);
277    }
278
279    @Override
280    public void addView(View child, int index, ViewGroup.LayoutParams params) {
281        if (getChildCount() > 0) {
282            throw new IllegalStateException("ScrollView can host only one direct child");
283        }
284
285        super.addView(child, index, params);
286    }
287
288    /**
289     * @return Returns true this ScrollView can be scrolled
290     */
291    private boolean canScroll() {
292        View child = getChildAt(0);
293        if (child != null) {
294            if (mHorizontal) {
295                return getWidth() < child.getWidth() + mPaddingLeft + mPaddingRight;
296            } else {
297                return getHeight() < child.getHeight() + mPaddingTop + mPaddingBottom;
298            }
299        }
300        return false;
301    }
302
303    /**
304     * Indicates whether this ScrollView's content is stretched to fill the viewport.
305     *
306     * @return True if the content fills the viewport, false otherwise.
307     *
308     * @attr ref android.R.styleable#ScrollView_fillViewport
309     */
310    public boolean isFillViewport() {
311        return mFillViewport;
312    }
313
314    /**
315     * Indicates this ScrollView whether it should stretch its content height to fill
316     * the viewport or not.
317     *
318     * @param fillViewport True to stretch the content's height to the viewport's
319     *        boundaries, false otherwise.
320     *
321     * @attr ref android.R.styleable#ScrollView_fillViewport
322     */
323    public void setFillViewport(boolean fillViewport) {
324        if (fillViewport != mFillViewport) {
325            mFillViewport = fillViewport;
326            requestLayout();
327        }
328    }
329
330    /**
331     * @return Whether arrow scrolling will animate its transition.
332     */
333    public boolean isSmoothScrollingEnabled() {
334        return mSmoothScrollingEnabled;
335    }
336
337    /**
338     * Set whether arrow scrolling will animate its transition.
339     * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
340     */
341    public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
342        mSmoothScrollingEnabled = smoothScrollingEnabled;
343    }
344
345    @Override
346    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
347        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
348
349        if (!mFillViewport) {
350            return;
351        }
352
353        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
354        if (heightMode == MeasureSpec.UNSPECIFIED) {
355            return;
356        }
357
358        if (getChildCount() > 0) {
359            final View child = getChildAt(0);
360            if (mHorizontal) {
361                int width = getMeasuredWidth();
362                if (child.getMeasuredWidth() < width) {
363                    final FrameLayout.LayoutParams lp = (LayoutParams) child
364                            .getLayoutParams();
365
366                    int childHeightMeasureSpec = getChildMeasureSpec(
367                            heightMeasureSpec, mPaddingTop + mPaddingBottom,
368                            lp.height);
369                    width -= mPaddingLeft;
370                    width -= mPaddingRight;
371                    int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
372                            width, MeasureSpec.EXACTLY);
373
374                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
375                }
376            } else {
377                int height = getMeasuredHeight();
378                if (child.getMeasuredHeight() < height) {
379                    final FrameLayout.LayoutParams lp = (LayoutParams) child
380                            .getLayoutParams();
381
382                    int childWidthMeasureSpec = getChildMeasureSpec(
383                            widthMeasureSpec, mPaddingLeft + mPaddingRight,
384                            lp.width);
385                    height -= mPaddingTop;
386                    height -= mPaddingBottom;
387                    int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
388                            height, MeasureSpec.EXACTLY);
389
390                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
391                }
392            }
393        }
394    }
395
396    @Override
397    public boolean dispatchKeyEvent(KeyEvent event) {
398        // Let the focused view and/or our descendants get the key first
399        return super.dispatchKeyEvent(event) || executeKeyEvent(event);
400    }
401
402    /**
403     * You can call this function yourself to have the scroll view perform
404     * scrolling from a key event, just as if the event had been dispatched to
405     * it by the view hierarchy.
406     *
407     * @param event The key event to execute.
408     * @return Return true if the event was handled, else false.
409     */
410    public boolean executeKeyEvent(KeyEvent event) {
411        mTempRect.setEmpty();
412
413        if (!canScroll()) {
414            if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
415                View currentFocused = findFocus();
416                if (currentFocused == this) currentFocused = null;
417                View nextFocused = FocusFinder.getInstance().findNextFocus(this,
418                        currentFocused, View.FOCUS_DOWN);
419                return nextFocused != null
420                        && nextFocused != this
421                        && nextFocused.requestFocus(View.FOCUS_DOWN);
422            }
423            return false;
424        }
425
426        boolean handled = false;
427        if (event.getAction() == KeyEvent.ACTION_DOWN) {
428            switch (event.getKeyCode()) {
429                case KeyEvent.KEYCODE_DPAD_UP:
430                    if (!event.isAltPressed()) {
431                        handled = arrowScroll(View.FOCUS_UP);
432                    } else {
433                        handled = fullScroll(View.FOCUS_UP);
434                    }
435                    break;
436                case KeyEvent.KEYCODE_DPAD_DOWN:
437                    if (!event.isAltPressed()) {
438                        handled = arrowScroll(View.FOCUS_DOWN);
439                    } else {
440                        handled = fullScroll(View.FOCUS_DOWN);
441                    }
442                    break;
443                case KeyEvent.KEYCODE_SPACE:
444                    pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
445                    break;
446            }
447        }
448
449        return handled;
450    }
451
452    private boolean inChild(int x, int y) {
453        if (getChildCount() > 0) {
454            final int scrollY = mScrollY;
455            final View child = getChildAt(0);
456            return !(y < child.getTop() - scrollY
457                    || y >= child.getBottom() - scrollY
458                    || x < child.getLeft()
459                    || x >= child.getRight());
460        }
461        return false;
462    }
463
464    private void initOrResetVelocityTracker() {
465        if (mVelocityTracker == null) {
466            mVelocityTracker = VelocityTracker.obtain();
467        } else {
468            mVelocityTracker.clear();
469        }
470    }
471
472    private void initVelocityTrackerIfNotExists() {
473        if (mVelocityTracker == null) {
474            mVelocityTracker = VelocityTracker.obtain();
475        }
476    }
477
478    private void recycleVelocityTracker() {
479        if (mVelocityTracker != null) {
480            mVelocityTracker.recycle();
481            mVelocityTracker = null;
482        }
483    }
484
485    @Override
486    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
487        if (disallowIntercept) {
488            recycleVelocityTracker();
489        }
490        super.requestDisallowInterceptTouchEvent(disallowIntercept);
491    }
492
493    @Override
494    public boolean onInterceptTouchEvent(MotionEvent ev) {
495        /*
496         * This method JUST determines whether we want to intercept the motion.
497         * If we return true, onMotionEvent will be called and we do the actual
498         * scrolling there.
499         */
500
501        /*
502         * Shortcut the most recurring case: the user is in the dragging state
503         * and he is moving his finger. We want to intercept this motion.
504         */
505        final int action = ev.getAction();
506        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
507            return true;
508        }
509        if ((action == MotionEvent.ACTION_MOVE) && (mIsOrthoDragged)) {
510            return true;
511        }
512        switch (action & MotionEvent.ACTION_MASK) {
513        case MotionEvent.ACTION_MOVE: {
514            /*
515             * mIsBeingDragged == false, otherwise the shortcut would have
516             * caught it. Check whether the user has moved far enough from his
517             * original down touch.
518             */
519
520            /*
521             * Locally do absolute value. mLastMotionY is set to the y value of
522             * the down event.
523             */
524            final int activePointerId = mActivePointerId;
525            if (activePointerId == INVALID_POINTER) {
526                // If we don't have a valid id, the touch down wasn't on
527                // content.
528                break;
529            }
530
531            final int pointerIndex = ev.findPointerIndex(activePointerId);
532            final float y = mHorizontal ? ev.getX(pointerIndex) : ev
533                    .getY(pointerIndex);
534            final int yDiff = (int) Math.abs(y - mLastMotionY);
535            if (yDiff > mTouchSlop) {
536                mIsBeingDragged = true;
537                mLastMotionY = y;
538                initVelocityTrackerIfNotExists();
539                mVelocityTracker.addMovement(ev);
540                if (mScrollStrictSpan == null) {
541                    mScrollStrictSpan = StrictMode
542                            .enterCriticalSpan("ScrollView-scroll");
543                }
544            } else {
545                final float ocoord = mHorizontal ? ev.getY(pointerIndex) : ev
546                        .getX(pointerIndex);
547                if (Math.abs(ocoord - mLastOrthoCoord) > mTouchSlop) {
548                    mIsOrthoDragged = true;
549                    mLastOrthoCoord = ocoord;
550                    initVelocityTrackerIfNotExists();
551                    mVelocityTracker.addMovement(ev);
552                }
553            }
554            break;
555        }
556
557        case MotionEvent.ACTION_DOWN: {
558            final float y = mHorizontal ? ev.getX() : ev.getY();
559            mDownCoords.x = ev.getX();
560            mDownCoords.y = ev.getY();
561            if (!inChild((int) ev.getX(), (int) ev.getY())) {
562                mIsBeingDragged = false;
563                recycleVelocityTracker();
564                break;
565            }
566
567            /*
568             * Remember location of down touch. ACTION_DOWN always refers to
569             * pointer index 0.
570             */
571            mLastMotionY = y;
572            mActivePointerId = ev.getPointerId(0);
573
574            initOrResetVelocityTracker();
575            mVelocityTracker.addMovement(ev);
576            /*
577             * If being flinged and user touches the screen, initiate drag;
578             * otherwise don't. mScroller.isFinished should be false when being
579             * flinged.
580             */
581            mIsBeingDragged = !mScroller.isFinished();
582            if (mIsBeingDragged && mScrollStrictSpan == null) {
583                mScrollStrictSpan = StrictMode
584                        .enterCriticalSpan("ScrollView-scroll");
585            }
586            mIsOrthoDragged = false;
587            final float ocoord = mHorizontal ? ev.getY() : ev.getX();
588            mLastOrthoCoord = ocoord;
589            mDownView = findViewAt((int) ev.getX(), (int) ev.getY());
590            break;
591        }
592
593        case MotionEvent.ACTION_CANCEL:
594        case MotionEvent.ACTION_UP:
595            /* Release the drag */
596            mIsBeingDragged = false;
597            mIsOrthoDragged = false;
598            mActivePointerId = INVALID_POINTER;
599            recycleVelocityTracker();
600            if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
601                    getScrollRange())) {
602                invalidate();
603            }
604            break;
605        case MotionEvent.ACTION_POINTER_UP:
606            onSecondaryPointerUp(ev);
607            break;
608        }
609
610        /*
611         * The only time we want to intercept motion events is if we are in the
612         * drag mode.
613         */
614        return mIsBeingDragged || mIsOrthoDragged;
615    }
616
617    @Override
618    public boolean onTouchEvent(MotionEvent ev) {
619        initVelocityTrackerIfNotExists();
620        mVelocityTracker.addMovement(ev);
621
622        final int action = ev.getAction();
623        switch (action & MotionEvent.ACTION_MASK) {
624            case MotionEvent.ACTION_DOWN: {
625                mIsBeingDragged = getChildCount() != 0;
626                if (!mIsBeingDragged) {
627                    return false;
628                }
629
630                /*
631                 * If being flinged and user touches, stop the fling. isFinished
632                 * will be false if being flinged.
633                 */
634                if (!mScroller.isFinished()) {
635                    mScroller.abortAnimation();
636                    if (mFlingStrictSpan != null) {
637                        mFlingStrictSpan.finish();
638                        mFlingStrictSpan = null;
639                    }
640                }
641
642                // Remember where the motion event started
643                mLastMotionY = mHorizontal ? ev.getX() : ev.getY();
644                mActivePointerId = ev.getPointerId(0);
645                break;
646            }
647            case MotionEvent.ACTION_MOVE:
648                if (mIsOrthoDragged) {
649                    // Scroll to follow the motion event
650                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
651                    final float x = ev.getX(activePointerIndex);
652                    final float y = ev.getY(activePointerIndex);
653                    if (isOrthoMove(x - mDownCoords.x, y - mDownCoords.y)) {
654                        onOrthoDrag(mDownView, mHorizontal
655                                ? y - mDownCoords.y
656                                : x - mDownCoords.x);
657                    }
658                } else if (mIsBeingDragged) {
659                    // Scroll to follow the motion event
660                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
661                    final float y = mHorizontal ? ev.getX(activePointerIndex)
662                            : ev.getY(activePointerIndex);
663                    final int deltaY = (int) (mLastMotionY - y);
664                    mLastMotionY = y;
665
666                    final int oldX = mScrollX;
667                    final int oldY = mScrollY;
668                    final int range = getScrollRange();
669                    if (mHorizontal) {
670                        if (overScrollBy(deltaY, 0, mScrollX, 0, range, 0,
671                                mOverscrollDistance, 0, true)) {
672                            // Break our velocity if we hit a scroll barrier.
673                            mVelocityTracker.clear();
674                        }
675                    } else {
676                        if (overScrollBy(0, deltaY, 0, mScrollY, 0, range,
677                                0, mOverscrollDistance, true)) {
678                            // Break our velocity if we hit a scroll barrier.
679                            mVelocityTracker.clear();
680                        }
681                    }
682                    onScrollChanged(mScrollX, mScrollY, oldX, oldY);
683
684                    final int overscrollMode = getOverScrollMode();
685                    if (overscrollMode == OVER_SCROLL_ALWAYS ||
686                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)) {
687                        final int pulledToY = mHorizontal ? oldX + deltaY : oldY + deltaY;
688                        if (pulledToY < 0) {
689                            onPull(pulledToY);
690                        } else if (pulledToY > range) {
691                            onPull(pulledToY - range);
692                        } else {
693                            onPull(0);
694                        }
695                    }
696                }
697                break;
698            case MotionEvent.ACTION_UP:
699                final VelocityTracker vtracker = mVelocityTracker;
700                vtracker.computeCurrentVelocity(1000, mMaximumVelocity);
701                if (isOrthoMove(vtracker.getXVelocity(mActivePointerId),
702                        vtracker.getYVelocity(mActivePointerId))
703                        && mMinimumVelocity < Math.abs((mHorizontal ? vtracker.getYVelocity()
704                                : vtracker.getXVelocity()))) {
705                    onOrthoFling(mDownView, mHorizontal ? vtracker.getYVelocity()
706                            : vtracker.getXVelocity());
707                    break;
708                }
709                if (mIsOrthoDragged) {
710                    onOrthoDragFinished(mDownView);
711                    mActivePointerId = INVALID_POINTER;
712                    endDrag();
713                } else if (mIsBeingDragged) {
714                    final VelocityTracker velocityTracker = mVelocityTracker;
715                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
716                    int initialVelocity = mHorizontal
717                            ? (int) velocityTracker.getXVelocity(mActivePointerId)
718                            : (int) velocityTracker.getYVelocity(mActivePointerId);
719
720                    if (getChildCount() > 0) {
721                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
722                            fling(-initialVelocity);
723                        } else {
724                            final int bottom = getScrollRange();
725                            if (mHorizontal) {
726                                if (mScroller.springBack(mScrollX, mScrollY, 0, bottom, 0, 0)) {
727                                    invalidate();
728                                }
729                            } else {
730                                if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, bottom)) {
731                                    invalidate();
732                                }
733                            }
734                        }
735                        onPull(0);
736                    }
737
738                    mActivePointerId = INVALID_POINTER;
739                    endDrag();
740                }
741                break;
742            case MotionEvent.ACTION_CANCEL:
743                if (mIsOrthoDragged) {
744                    onOrthoDragFinished(mDownView);
745                    mActivePointerId = INVALID_POINTER;
746                    endDrag();
747                } else if (mIsBeingDragged && getChildCount() > 0) {
748                    if (mHorizontal) {
749                        if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
750                            invalidate();
751                        }
752                    } else {
753                        if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
754                            invalidate();
755                        }
756                    }
757                    mActivePointerId = INVALID_POINTER;
758                    endDrag();
759                }
760                break;
761            case MotionEvent.ACTION_POINTER_DOWN: {
762                final int index = ev.getActionIndex();
763                final float y = mHorizontal ? ev.getX(index) : ev.getY(index);
764                mLastMotionY = y;
765                mLastOrthoCoord = mHorizontal ? ev.getY(index) : ev.getX(index);
766                mActivePointerId = ev.getPointerId(index);
767                break;
768            }
769            case MotionEvent.ACTION_POINTER_UP:
770                onSecondaryPointerUp(ev);
771                mLastMotionY = mHorizontal
772                        ? ev.getX(ev.findPointerIndex(mActivePointerId))
773                        : ev.getY(ev.findPointerIndex(mActivePointerId));
774                break;
775        }
776        return true;
777    }
778
779    protected View findViewAt(int x, int y) {
780        // subclass responsibility
781        return null;
782    }
783
784    protected void onPull(int delta) {
785    }
786
787    private void onSecondaryPointerUp(MotionEvent ev) {
788        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
789                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
790        final int pointerId = ev.getPointerId(pointerIndex);
791        if (pointerId == mActivePointerId) {
792            // This was our active pointer going up. Choose a new
793            // active pointer and adjust accordingly.
794            // TODO: Make this decision more intelligent.
795            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
796            mLastMotionY = mHorizontal ? ev.getX(newPointerIndex) : ev.getY(newPointerIndex);
797            mActivePointerId = ev.getPointerId(newPointerIndex);
798            if (mVelocityTracker != null) {
799                mVelocityTracker.clear();
800            }
801            mLastOrthoCoord = mHorizontal ? ev.getY(newPointerIndex)
802                    : ev.getX(newPointerIndex);
803        }
804    }
805
806    @Override
807    public boolean onGenericMotionEvent(MotionEvent event) {
808        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
809            switch (event.getAction()) {
810            case MotionEvent.ACTION_SCROLL: {
811                if (!mIsBeingDragged) {
812                    if (mHorizontal) {
813                        final float hscroll = event
814                                .getAxisValue(MotionEvent.AXIS_HSCROLL);
815                        if (hscroll != 0) {
816                            final int delta = (int) (hscroll * getHorizontalScrollFactor());
817                            final int range = getScrollRange();
818                            int oldScrollX = mScrollX;
819                            int newScrollX = oldScrollX - delta;
820                            if (newScrollX < 0) {
821                                newScrollX = 0;
822                            } else if (newScrollX > range) {
823                                newScrollX = range;
824                            }
825                            if (newScrollX != oldScrollX) {
826                                super.scrollTo(newScrollX, mScrollY);
827                                return true;
828                            }
829                        }
830                    } else {
831                        final float vscroll = event
832                                .getAxisValue(MotionEvent.AXIS_VSCROLL);
833                        if (vscroll != 0) {
834                            final int delta = (int) (vscroll * getVerticalScrollFactor());
835                            final int range = getScrollRange();
836                            int oldScrollY = mScrollY;
837                            int newScrollY = oldScrollY - delta;
838                            if (newScrollY < 0) {
839                                newScrollY = 0;
840                            } else if (newScrollY > range) {
841                                newScrollY = range;
842                            }
843                            if (newScrollY != oldScrollY) {
844                                super.scrollTo(mScrollX, newScrollY);
845                                return true;
846                            }
847                        }
848                    }
849                }
850            }
851            }
852        }
853        return super.onGenericMotionEvent(event);
854    }
855
856    protected void onOrthoDrag(View draggedView, float distance) {
857    }
858
859    protected void onOrthoDragFinished(View draggedView) {
860    }
861
862    protected void onOrthoFling(View draggedView, float velocity) {
863    }
864
865    @Override
866    protected void onOverScrolled(int scrollX, int scrollY,
867            boolean clampedX, boolean clampedY) {
868        // Treat animating scrolls differently; see #computeScroll() for why.
869        if (!mScroller.isFinished()) {
870            mScrollX = scrollX;
871            mScrollY = scrollY;
872            invalidateParentIfNeeded();
873            if (mHorizontal && clampedX) {
874                mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0);
875            } else if (!mHorizontal && clampedY) {
876                mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
877            }
878        } else {
879            super.scrollTo(scrollX, scrollY);
880        }
881        awakenScrollBars();
882    }
883
884    @Override
885    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
886        super.onInitializeAccessibilityNodeInfo(info);
887        info.setScrollable(true);
888    }
889
890    @Override
891    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
892        super.onInitializeAccessibilityEvent(event);
893        event.setScrollable(true);
894    }
895
896    @Override
897    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
898        // Do not append text content to scroll events they are fired frequently
899        // and the client has already received another event type with the text.
900        if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_SCROLLED) {
901            super.dispatchPopulateAccessibilityEvent(event);
902        }
903        return false;
904    }
905
906    private int getScrollRange() {
907        int scrollRange = 0;
908        if (getChildCount() > 0) {
909            View child = getChildAt(0);
910            if (mHorizontal) {
911                scrollRange = Math.max(0,
912                        child.getWidth() - (getWidth() - mPaddingRight - mPaddingLeft));
913            } else {
914                scrollRange = Math.max(0,
915                        child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop));
916            }
917        }
918        return scrollRange;
919    }
920
921    /**
922     * <p>
923     * Finds the next focusable component that fits in this View's bounds
924     * (excluding fading edges) pretending that this View's top is located at
925     * the parameter top.
926     * </p>
927     *
928     * @param topFocus           look for a candidate at the top of the bounds if topFocus is true,
929     *                           or at the bottom of the bounds if topFocus is false
930     * @param top                the top offset of the bounds in which a focusable must be
931     *                           found (the fading edge is assumed to start at this position)
932     * @param preferredFocusable the View that has highest priority and will be
933     *                           returned if it is within my bounds (null is valid)
934     * @return the next focusable component in the bounds or null if none can be found
935     */
936    private View findFocusableViewInMyBounds(final boolean topFocus,
937            final int top, View preferredFocusable) {
938        /*
939         * The fading edge's transparent side should be considered for focus
940         * since it's mostly visible, so we divide the actual fading edge length
941         * by 2.
942         */
943        final int fadingEdgeLength = (mHorizontal
944                ? getHorizontalFadingEdgeLength()
945                : getVerticalFadingEdgeLength()) / 2;
946        final int topWithoutFadingEdge = top + fadingEdgeLength;
947        final int bottomWithoutFadingEdge = top + (mHorizontal ? getWidth() : getHeight()) - fadingEdgeLength;
948
949        if ((preferredFocusable != null)
950                && ((mHorizontal ? preferredFocusable.getLeft() : preferredFocusable.getTop())
951                        < bottomWithoutFadingEdge)
952                && ((mHorizontal ? preferredFocusable.getRight() : preferredFocusable.getBottom()) > topWithoutFadingEdge)) {
953            return preferredFocusable;
954        }
955
956        return findFocusableViewInBounds(topFocus, topWithoutFadingEdge,
957                bottomWithoutFadingEdge);
958    }
959
960    /**
961     * <p>
962     * Finds the next focusable component that fits in the specified bounds.
963     * </p>
964     *
965     * @param topFocus look for a candidate is the one at the top of the bounds
966     *                 if topFocus is true, or at the bottom of the bounds if topFocus is
967     *                 false
968     * @param top      the top offset of the bounds in which a focusable must be
969     *                 found
970     * @param bottom   the bottom offset of the bounds in which a focusable must
971     *                 be found
972     * @return the next focusable component in the bounds or null if none can
973     *         be found
974     */
975    private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) {
976
977        List<View> focusables = getFocusables(View.FOCUS_FORWARD);
978        View focusCandidate = null;
979
980        /*
981         * A fully contained focusable is one where its top is below the bound's
982         * top, and its bottom is above the bound's bottom. A partially
983         * contained focusable is one where some part of it is within the
984         * bounds, but it also has some part that is not within bounds.  A fully contained
985         * focusable is preferred to a partially contained focusable.
986         */
987        boolean foundFullyContainedFocusable = false;
988
989        int count = focusables.size();
990        for (int i = 0; i < count; i++) {
991            View view = focusables.get(i);
992            int viewTop = mHorizontal ? view.getLeft() : view.getTop();
993            int viewBottom = mHorizontal ? view.getRight() : view.getBottom();
994
995            if (top < viewBottom && viewTop < bottom) {
996                /*
997                 * the focusable is in the target area, it is a candidate for
998                 * focusing
999                 */
1000
1001                final boolean viewIsFullyContained = (top < viewTop) &&
1002                        (viewBottom < bottom);
1003
1004                if (focusCandidate == null) {
1005                    /* No candidate, take this one */
1006                    focusCandidate = view;
1007                    foundFullyContainedFocusable = viewIsFullyContained;
1008                } else {
1009                    final int ctop = mHorizontal ? focusCandidate.getLeft() : focusCandidate.getTop();
1010                    final int cbot = mHorizontal ? focusCandidate.getRight() : focusCandidate.getBottom();
1011                    final boolean viewIsCloserToBoundary =
1012                            (topFocus && viewTop < ctop) ||
1013                                    (!topFocus && viewBottom > cbot);
1014
1015                    if (foundFullyContainedFocusable) {
1016                        if (viewIsFullyContained && viewIsCloserToBoundary) {
1017                            /*
1018                             * We're dealing with only fully contained views, so
1019                             * it has to be closer to the boundary to beat our
1020                             * candidate
1021                             */
1022                            focusCandidate = view;
1023                        }
1024                    } else {
1025                        if (viewIsFullyContained) {
1026                            /* Any fully contained view beats a partially contained view */
1027                            focusCandidate = view;
1028                            foundFullyContainedFocusable = true;
1029                        } else if (viewIsCloserToBoundary) {
1030                            /*
1031                             * Partially contained view beats another partially
1032                             * contained view if it's closer
1033                             */
1034                            focusCandidate = view;
1035                        }
1036                    }
1037                }
1038            }
1039        }
1040
1041        return focusCandidate;
1042    }
1043
1044    // i was here
1045
1046    /**
1047     * <p>Handles scrolling in response to a "page up/down" shortcut press. This
1048     * method will scroll the view by one page up or down and give the focus
1049     * to the topmost/bottommost component in the new visible area. If no
1050     * component is a good candidate for focus, this scrollview reclaims the
1051     * focus.</p>
1052     *
1053     * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
1054     *                  to go one page up or
1055     *                  {@link android.view.View#FOCUS_DOWN} to go one page down
1056     * @return true if the key event is consumed by this method, false otherwise
1057     */
1058    public boolean pageScroll(int direction) {
1059        boolean down = direction == View.FOCUS_DOWN;
1060        int height = getHeight();
1061
1062        if (down) {
1063            mTempRect.top = getScrollY() + height;
1064            int count = getChildCount();
1065            if (count > 0) {
1066                View view = getChildAt(count - 1);
1067                if (mTempRect.top + height > view.getBottom()) {
1068                    mTempRect.top = view.getBottom() - height;
1069                }
1070            }
1071        } else {
1072            mTempRect.top = getScrollY() - height;
1073            if (mTempRect.top < 0) {
1074                mTempRect.top = 0;
1075            }
1076        }
1077        mTempRect.bottom = mTempRect.top + height;
1078
1079        return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
1080    }
1081
1082    /**
1083     * <p>Handles scrolling in response to a "home/end" shortcut press. This
1084     * method will scroll the view to the top or bottom and give the focus
1085     * to the topmost/bottommost component in the new visible area. If no
1086     * component is a good candidate for focus, this scrollview reclaims the
1087     * focus.</p>
1088     *
1089     * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
1090     *                  to go the top of the view or
1091     *                  {@link android.view.View#FOCUS_DOWN} to go the bottom
1092     * @return true if the key event is consumed by this method, false otherwise
1093     */
1094    public boolean fullScroll(int direction) {
1095        boolean down = direction == View.FOCUS_DOWN;
1096        int height = getHeight();
1097
1098        mTempRect.top = 0;
1099        mTempRect.bottom = height;
1100
1101        if (down) {
1102            int count = getChildCount();
1103            if (count > 0) {
1104                View view = getChildAt(count - 1);
1105                mTempRect.bottom = view.getBottom() + mPaddingBottom;
1106                mTempRect.top = mTempRect.bottom - height;
1107            }
1108        }
1109
1110        return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
1111    }
1112
1113    /**
1114     * <p>Scrolls the view to make the area defined by <code>top</code> and
1115     * <code>bottom</code> visible. This method attempts to give the focus
1116     * to a component visible in this area. If no component can be focused in
1117     * the new visible area, the focus is reclaimed by this ScrollView.</p>
1118     *
1119     * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
1120     *                  to go upward, {@link android.view.View#FOCUS_DOWN} to downward
1121     * @param top       the top offset of the new area to be made visible
1122     * @param bottom    the bottom offset of the new area to be made visible
1123     * @return true if the key event is consumed by this method, false otherwise
1124     */
1125    private boolean scrollAndFocus(int direction, int top, int bottom) {
1126        boolean handled = true;
1127
1128        int height = getHeight();
1129        int containerTop = getScrollY();
1130        int containerBottom = containerTop + height;
1131        boolean up = direction == View.FOCUS_UP;
1132
1133        View newFocused = findFocusableViewInBounds(up, top, bottom);
1134        if (newFocused == null) {
1135            newFocused = this;
1136        }
1137
1138        if (top >= containerTop && bottom <= containerBottom) {
1139            handled = false;
1140        } else {
1141            int delta = up ? (top - containerTop) : (bottom - containerBottom);
1142            doScrollY(delta);
1143        }
1144
1145        if (newFocused != findFocus()) newFocused.requestFocus(direction);
1146
1147        return handled;
1148    }
1149
1150    /**
1151     * Handle scrolling in response to an up or down arrow click.
1152     *
1153     * @param direction The direction corresponding to the arrow key that was
1154     *                  pressed
1155     * @return True if we consumed the event, false otherwise
1156     */
1157    public boolean arrowScroll(int direction) {
1158
1159        View currentFocused = findFocus();
1160        if (currentFocused == this) currentFocused = null;
1161
1162        View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
1163
1164        final int maxJump = getMaxScrollAmount();
1165
1166        if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) {
1167            nextFocused.getDrawingRect(mTempRect);
1168            offsetDescendantRectToMyCoords(nextFocused, mTempRect);
1169            int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1170            doScrollY(scrollDelta);
1171            nextFocused.requestFocus(direction);
1172        } else {
1173            // no new focus
1174            int scrollDelta = maxJump;
1175
1176            if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
1177                scrollDelta = getScrollY();
1178            } else if (direction == View.FOCUS_DOWN) {
1179                if (getChildCount() > 0) {
1180                    int daBottom = getChildAt(0).getBottom();
1181                    int screenBottom = getScrollY() + getHeight() - mPaddingBottom;
1182                    if (daBottom - screenBottom < maxJump) {
1183                        scrollDelta = daBottom - screenBottom;
1184                    }
1185                }
1186            }
1187            if (scrollDelta == 0) {
1188                return false;
1189            }
1190            doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
1191        }
1192
1193        if (currentFocused != null && currentFocused.isFocused()
1194                && isOffScreen(currentFocused)) {
1195            // previously focused item still has focus and is off screen, give
1196            // it up (take it back to ourselves)
1197            // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
1198            // sure to
1199            // get it)
1200            final int descendantFocusability = getDescendantFocusability();  // save
1201            setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
1202            requestFocus();
1203            setDescendantFocusability(descendantFocusability);  // restore
1204        }
1205        return true;
1206    }
1207
1208    private boolean isOrthoMove(float moveX, float moveY) {
1209        return mHorizontal && Math.abs(moveY) > Math.abs(moveX)
1210                || !mHorizontal && Math.abs(moveX) > Math.abs(moveY);
1211    }
1212
1213    /**
1214     * @return whether the descendant of this scroll view is scrolled off
1215     *  screen.
1216     */
1217    private boolean isOffScreen(View descendant) {
1218        if (mHorizontal) {
1219            return !isWithinDeltaOfScreen(descendant, getWidth(), 0);
1220        } else {
1221            return !isWithinDeltaOfScreen(descendant, 0, getHeight());
1222        }
1223    }
1224
1225    /**
1226     * @return whether the descendant of this scroll view is within delta
1227     *  pixels of being on the screen.
1228     */
1229    private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) {
1230        descendant.getDrawingRect(mTempRect);
1231        offsetDescendantRectToMyCoords(descendant, mTempRect);
1232        if (mHorizontal) {
1233            return (mTempRect.right + delta) >= getScrollX()
1234            && (mTempRect.left - delta) <= (getScrollX() + height);
1235        } else {
1236            return (mTempRect.bottom + delta) >= getScrollY()
1237            && (mTempRect.top - delta) <= (getScrollY() + height);
1238        }
1239    }
1240
1241    /**
1242     * Smooth scroll by a Y delta
1243     *
1244     * @param delta the number of pixels to scroll by on the Y axis
1245     */
1246    private void doScrollY(int delta) {
1247        if (delta != 0) {
1248            if (mSmoothScrollingEnabled) {
1249                if (mHorizontal) {
1250                    smoothScrollBy(0, delta);
1251                } else {
1252                    smoothScrollBy(delta, 0);
1253                }
1254            } else {
1255                if (mHorizontal) {
1256                    scrollBy(0, delta);
1257                } else {
1258                    scrollBy(delta, 0);
1259                }
1260            }
1261        }
1262    }
1263
1264    /**
1265     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
1266     *
1267     * @param dx the number of pixels to scroll by on the X axis
1268     * @param dy the number of pixels to scroll by on the Y axis
1269     */
1270    public final void smoothScrollBy(int dx, int dy) {
1271        if (getChildCount() == 0) {
1272            // Nothing to do.
1273            return;
1274        }
1275        long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
1276        if (duration > ANIMATED_SCROLL_GAP) {
1277            if (mHorizontal) {
1278                final int width = getWidth() - mPaddingRight - mPaddingLeft;
1279                final int right = getChildAt(0).getWidth();
1280                final int maxX = Math.max(0, right - width);
1281                final int scrollX = mScrollX;
1282                dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
1283                mScroller.startScroll(scrollX, mScrollY, dx, 0);
1284            } else {
1285                final int height = getHeight() - mPaddingBottom - mPaddingTop;
1286                final int bottom = getChildAt(0).getHeight();
1287                final int maxY = Math.max(0, bottom - height);
1288                final int scrollY = mScrollY;
1289                dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
1290                mScroller.startScroll(mScrollX, scrollY, 0, dy);
1291            }
1292            invalidate();
1293        } else {
1294            if (!mScroller.isFinished()) {
1295                mScroller.abortAnimation();
1296                if (mFlingStrictSpan != null) {
1297                    mFlingStrictSpan.finish();
1298                    mFlingStrictSpan = null;
1299                }
1300            }
1301            scrollBy(dx, dy);
1302        }
1303        mLastScroll = AnimationUtils.currentAnimationTimeMillis();
1304    }
1305
1306    /**
1307     * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
1308     *
1309     * @param x the position where to scroll on the X axis
1310     * @param y the position where to scroll on the Y axis
1311     */
1312    public final void smoothScrollTo(int x, int y) {
1313        smoothScrollBy(x - mScrollX, y - mScrollY);
1314    }
1315
1316    /**
1317     * <p>
1318     * The scroll range of a scroll view is the overall height of all of its
1319     * children.
1320     * </p>
1321     */
1322    @Override
1323    protected int computeVerticalScrollRange() {
1324        if (mHorizontal) {
1325            return super.computeVerticalScrollRange();
1326        }
1327        final int count = getChildCount();
1328        final int contentHeight = getHeight() - mPaddingBottom - mPaddingTop;
1329        if (count == 0) {
1330            return contentHeight;
1331        }
1332
1333        int scrollRange = getChildAt(0).getBottom();
1334        final int scrollY = mScrollY;
1335        final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
1336        if (scrollY < 0) {
1337            scrollRange -= scrollY;
1338        } else if (scrollY > overscrollBottom) {
1339            scrollRange += scrollY - overscrollBottom;
1340        }
1341
1342        return scrollRange;
1343    }
1344
1345    /**
1346     * <p>
1347     * The scroll range of a scroll view is the overall height of all of its
1348     * children.
1349     * </p>
1350     */
1351    @Override
1352    protected int computeHorizontalScrollRange() {
1353        if (!mHorizontal) {
1354            return super.computeHorizontalScrollRange();
1355        }
1356        final int count = getChildCount();
1357        final int contentWidth = getWidth() - mPaddingRight - mPaddingLeft;
1358        if (count == 0) {
1359            return contentWidth;
1360        }
1361
1362        int scrollRange = getChildAt(0).getRight();
1363        final int scrollX = mScrollX;
1364        final int overscrollBottom = Math.max(0, scrollRange - contentWidth);
1365        if (scrollX < 0) {
1366            scrollRange -= scrollX;
1367        } else if (scrollX > overscrollBottom) {
1368            scrollRange += scrollX - overscrollBottom;
1369        }
1370
1371        return scrollRange;
1372    }
1373
1374    @Override
1375    protected int computeVerticalScrollOffset() {
1376        return Math.max(0, super.computeVerticalScrollOffset());
1377    }
1378
1379    @Override
1380    protected int computeHorizontalScrollOffset() {
1381        return Math.max(0, super.computeHorizontalScrollOffset());
1382    }
1383
1384    @Override
1385    protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
1386        ViewGroup.LayoutParams lp = child.getLayoutParams();
1387
1388        int childWidthMeasureSpec;
1389        int childHeightMeasureSpec;
1390
1391        if (mHorizontal) {
1392            childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop
1393                    + mPaddingBottom, lp.height);
1394
1395            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1396        } else {
1397            childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
1398                    + mPaddingRight, lp.width);
1399
1400            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1401        }
1402
1403        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1404    }
1405
1406    @Override
1407    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
1408            int parentHeightMeasureSpec, int heightUsed) {
1409        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
1410
1411        int childWidthMeasureSpec;
1412        int childHeightMeasureSpec;
1413        if (mHorizontal) {
1414            childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
1415                    mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
1416                            + heightUsed, lp.height);
1417            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
1418                    lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
1419        } else {
1420            childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
1421                    mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
1422                            + widthUsed, lp.width);
1423            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
1424                    lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
1425        }
1426        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1427    }
1428
1429    @Override
1430    public void computeScroll() {
1431        if (mScroller.computeScrollOffset()) {
1432            // This is called at drawing time by ViewGroup.  We don't want to
1433            // re-show the scrollbars at this point, which scrollTo will do,
1434            // so we replicate most of scrollTo here.
1435            //
1436            //         It's a little odd to call onScrollChanged from inside the drawing.
1437            //
1438            //         It is, except when you remember that computeScroll() is used to
1439            //         animate scrolling. So unless we want to defer the onScrollChanged()
1440            //         until the end of the animated scrolling, we don't really have a
1441            //         choice here.
1442            //
1443            //         I agree.  The alternative, which I think would be worse, is to post
1444            //         something and tell the subclasses later.  This is bad because there
1445            //         will be a window where mScrollX/Y is different from what the app
1446            //         thinks it is.
1447            //
1448            int oldX = mScrollX;
1449            int oldY = mScrollY;
1450            int x = mScroller.getCurrX();
1451            int y = mScroller.getCurrY();
1452
1453            if (oldX != x || oldY != y) {
1454                if (mHorizontal) {
1455                    overScrollBy(x - oldX, y - oldY, oldX, oldY, getScrollRange(), 0,
1456                            mOverflingDistance, 0, false);
1457                } else {
1458                    overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, getScrollRange(),
1459                            0, mOverflingDistance, false);
1460                }
1461                onScrollChanged(mScrollX, mScrollY, oldX, oldY);
1462            }
1463            awakenScrollBars();
1464
1465            // Keep on drawing until the animation has finished.
1466            postInvalidate();
1467        } else {
1468            if (mFlingStrictSpan != null) {
1469                mFlingStrictSpan.finish();
1470                mFlingStrictSpan = null;
1471            }
1472        }
1473    }
1474
1475    /**
1476     * Scrolls the view to the given child.
1477     *
1478     * @param child the View to scroll to
1479     */
1480    private void scrollToChild(View child) {
1481        child.getDrawingRect(mTempRect);
1482
1483        /* Offset from child's local coordinates to ScrollView coordinates */
1484        offsetDescendantRectToMyCoords(child, mTempRect);
1485        scrollToChildRect(mTempRect, true);
1486    }
1487
1488    /**
1489     * If rect is off screen, scroll just enough to get it (or at least the
1490     * first screen size chunk of it) on screen.
1491     *
1492     * @param rect      The rectangle.
1493     * @param immediate True to scroll immediately without animation
1494     * @return true if scrolling was performed
1495     */
1496    private boolean scrollToChildRect(Rect rect, boolean immediate) {
1497        final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
1498        final boolean scroll = delta != 0;
1499        if (scroll) {
1500            if (immediate) {
1501                if (mHorizontal) {
1502                    scrollBy(delta, 0);
1503                } else {
1504                    scrollBy(0, delta);
1505                }
1506            } else {
1507                if (mHorizontal) {
1508                    smoothScrollBy(delta, 0);
1509                } else {
1510                    smoothScrollBy(0, delta);
1511                }
1512            }
1513        }
1514        return scroll;
1515    }
1516
1517    /**
1518     * Compute the amount to scroll in the Y direction in order to get
1519     * a rectangle completely on the screen (or, if taller than the screen,
1520     * at least the first screen size chunk of it).
1521     *
1522     * @param rect The rect.
1523     * @return The scroll delta.
1524     */
1525    protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
1526        if (mHorizontal) {
1527            return computeScrollDeltaToGetChildRectOnScreenHorizontal(rect);
1528        } else {
1529            return computeScrollDeltaToGetChildRectOnScreenVertical(rect);
1530        }
1531    }
1532
1533    private int computeScrollDeltaToGetChildRectOnScreenVertical(Rect rect) {
1534        if (getChildCount() == 0) return 0;
1535
1536        int height = getHeight();
1537        int screenTop = getScrollY();
1538        int screenBottom = screenTop + height;
1539
1540        int fadingEdge = getVerticalFadingEdgeLength();
1541
1542        // leave room for top fading edge as long as rect isn't at very top
1543        if (rect.top > 0) {
1544            screenTop += fadingEdge;
1545        }
1546
1547        // leave room for bottom fading edge as long as rect isn't at very bottom
1548        if (rect.bottom < getChildAt(0).getHeight()) {
1549            screenBottom -= fadingEdge;
1550        }
1551
1552        int scrollYDelta = 0;
1553
1554        if (rect.bottom > screenBottom && rect.top > screenTop) {
1555            // need to move down to get it in view: move down just enough so
1556            // that the entire rectangle is in view (or at least the first
1557            // screen size chunk).
1558
1559            if (rect.height() > height) {
1560                // just enough to get screen size chunk on
1561                scrollYDelta += (rect.top - screenTop);
1562            } else {
1563                // get entire rect at bottom of screen
1564                scrollYDelta += (rect.bottom - screenBottom);
1565            }
1566
1567            // make sure we aren't scrolling beyond the end of our content
1568            int bottom = getChildAt(0).getBottom();
1569            int distanceToBottom = bottom - screenBottom;
1570            scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
1571
1572        } else if (rect.top < screenTop && rect.bottom < screenBottom) {
1573            // need to move up to get it in view: move up just enough so that
1574            // entire rectangle is in view (or at least the first screen
1575            // size chunk of it).
1576
1577            if (rect.height() > height) {
1578                // screen size chunk
1579                scrollYDelta -= (screenBottom - rect.bottom);
1580            } else {
1581                // entire rect at top
1582                scrollYDelta -= (screenTop - rect.top);
1583            }
1584
1585            // make sure we aren't scrolling any further than the top our content
1586            scrollYDelta = Math.max(scrollYDelta, -getScrollY());
1587        }
1588        return scrollYDelta;
1589    }
1590
1591    private int computeScrollDeltaToGetChildRectOnScreenHorizontal(Rect rect) {
1592        if (getChildCount() == 0) return 0;
1593
1594        int width = getWidth();
1595        int screenLeft = getScrollX();
1596        int screenRight = screenLeft + width;
1597
1598        int fadingEdge = getHorizontalFadingEdgeLength();
1599
1600        // leave room for left fading edge as long as rect isn't at very left
1601        if (rect.left > 0) {
1602            screenLeft += fadingEdge;
1603        }
1604
1605        // leave room for right fading edge as long as rect isn't at very right
1606        if (rect.right < getChildAt(0).getWidth()) {
1607            screenRight -= fadingEdge;
1608        }
1609
1610        int scrollXDelta = 0;
1611
1612        if (rect.right > screenRight && rect.left > screenLeft) {
1613            // need to move right to get it in view: move right just enough so
1614            // that the entire rectangle is in view (or at least the first
1615            // screen size chunk).
1616
1617            if (rect.width() > width) {
1618                // just enough to get screen size chunk on
1619                scrollXDelta += (rect.left - screenLeft);
1620            } else {
1621                // get entire rect at right of screen
1622                scrollXDelta += (rect.right - screenRight);
1623            }
1624
1625            // make sure we aren't scrolling beyond the end of our content
1626            int right = getChildAt(0).getRight();
1627            int distanceToRight = right - screenRight;
1628            scrollXDelta = Math.min(scrollXDelta, distanceToRight);
1629
1630        } else if (rect.left < screenLeft && rect.right < screenRight) {
1631            // need to move right to get it in view: move right just enough so that
1632            // entire rectangle is in view (or at least the first screen
1633            // size chunk of it).
1634
1635            if (rect.width() > width) {
1636                // screen size chunk
1637                scrollXDelta -= (screenRight - rect.right);
1638            } else {
1639                // entire rect at left
1640                scrollXDelta -= (screenLeft - rect.left);
1641            }
1642
1643            // make sure we aren't scrolling any further than the left our content
1644            scrollXDelta = Math.max(scrollXDelta, -getScrollX());
1645        }
1646        return scrollXDelta;
1647    }
1648
1649
1650    @Override
1651    public void requestChildFocus(View child, View focused) {
1652        if (!mIsLayoutDirty) {
1653            scrollToChild(focused);
1654        } else {
1655            // The child may not be laid out yet, we can't compute the scroll yet
1656            mChildToScrollTo = focused;
1657        }
1658        super.requestChildFocus(child, focused);
1659    }
1660
1661
1662    /**
1663     * When looking for focus in children of a scroll view, need to be a little
1664     * more careful not to give focus to something that is scrolled off screen.
1665     *
1666     * This is more expensive than the default {@link android.view.ViewGroup}
1667     * implementation, otherwise this behavior might have been made the default.
1668     */
1669    @Override
1670    protected boolean onRequestFocusInDescendants(int direction,
1671            Rect previouslyFocusedRect) {
1672
1673        // convert from forward / backward notation to up / down / left / right
1674        // (ugh).
1675        if (mHorizontal) {
1676            if (direction == View.FOCUS_FORWARD) {
1677                direction = View.FOCUS_RIGHT;
1678            } else if (direction == View.FOCUS_BACKWARD) {
1679                direction = View.FOCUS_LEFT;
1680            }
1681        } else {
1682            if (direction == View.FOCUS_FORWARD) {
1683                direction = View.FOCUS_DOWN;
1684            } else if (direction == View.FOCUS_BACKWARD) {
1685                direction = View.FOCUS_UP;
1686            }
1687        }
1688
1689        final View nextFocus = previouslyFocusedRect == null ?
1690                FocusFinder.getInstance().findNextFocus(this, null, direction) :
1691                FocusFinder.getInstance().findNextFocusFromRect(this,
1692                        previouslyFocusedRect, direction);
1693
1694        if (nextFocus == null) {
1695            return false;
1696        }
1697
1698        if (isOffScreen(nextFocus)) {
1699            return false;
1700        }
1701
1702        return nextFocus.requestFocus(direction, previouslyFocusedRect);
1703    }
1704
1705    @Override
1706    public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
1707            boolean immediate) {
1708        // offset into coordinate space of this scroll view
1709        rectangle.offset(child.getLeft() - child.getScrollX(),
1710                child.getTop() - child.getScrollY());
1711
1712        return scrollToChildRect(rectangle, immediate);
1713    }
1714
1715    @Override
1716    public void requestLayout() {
1717        mIsLayoutDirty = true;
1718        super.requestLayout();
1719    }
1720
1721    @Override
1722    protected void onDetachedFromWindow() {
1723        super.onDetachedFromWindow();
1724
1725        if (mScrollStrictSpan != null) {
1726            mScrollStrictSpan.finish();
1727            mScrollStrictSpan = null;
1728        }
1729        if (mFlingStrictSpan != null) {
1730            mFlingStrictSpan.finish();
1731            mFlingStrictSpan = null;
1732        }
1733    }
1734
1735    @Override
1736    protected void onLayout(boolean changed, int l, int t, int r, int b) {
1737        super.onLayout(changed, l, t, r, b);
1738        mIsLayoutDirty = false;
1739        // Give a child focus if it needs it
1740        if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
1741            scrollToChild(mChildToScrollTo);
1742        }
1743        mChildToScrollTo = null;
1744
1745        // Calling this with the present values causes it to re-clam them
1746        scrollTo(mScrollX, mScrollY);
1747    }
1748
1749    @Override
1750    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1751        super.onSizeChanged(w, h, oldw, oldh);
1752
1753        View currentFocused = findFocus();
1754        if (null == currentFocused || this == currentFocused)
1755            return;
1756
1757        // If the currently-focused view was visible on the screen when the
1758        // screen was at the old height, then scroll the screen to make that
1759        // view visible with the new screen height.
1760        if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) {
1761            currentFocused.getDrawingRect(mTempRect);
1762            offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1763            int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1764            doScrollY(scrollDelta);
1765        }
1766    }
1767
1768    /**
1769     * Return true if child is an descendant of parent, (or equal to the parent).
1770     */
1771    private boolean isViewDescendantOf(View child, View parent) {
1772        if (child == parent) {
1773            return true;
1774        }
1775
1776        final ViewParent theParent = child.getParent();
1777        return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1778    }
1779
1780    /**
1781     * Fling the scroll view
1782     *
1783     * @param velocityY The initial velocity in the Y direction. Positive
1784     *                  numbers mean that the finger/cursor is moving down the screen,
1785     *                  which means we want to scroll towards the top.
1786     */
1787    public void fling(int velocityY) {
1788        if (getChildCount() > 0) {
1789            if (mHorizontal) {
1790                int width = getWidth() - mPaddingRight - mPaddingLeft;
1791                int right = getChildAt(0).getWidth();
1792
1793                mScroller.fling(mScrollX, mScrollY, velocityY, 0,
1794                        0, Math.max(0, right - width), 0, 0, width/2, 0);
1795            } else {
1796                int height = getHeight() - mPaddingBottom - mPaddingTop;
1797                int bottom = getChildAt(0).getHeight();
1798
1799                mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
1800                        Math.max(0, bottom - height), 0, height/2);
1801            }
1802            if (mFlingStrictSpan == null) {
1803                mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
1804            }
1805
1806            invalidate();
1807        }
1808    }
1809
1810    private void endDrag() {
1811        mIsBeingDragged = false;
1812        mIsOrthoDragged = false;
1813        mDownView = null;
1814        recycleVelocityTracker();
1815        if (mScrollStrictSpan != null) {
1816            mScrollStrictSpan.finish();
1817            mScrollStrictSpan = null;
1818        }
1819    }
1820
1821    /**
1822     * {@inheritDoc}
1823     *
1824     * <p>This version also clamps the scrolling to the bounds of our child.
1825     */
1826    @Override
1827    public void scrollTo(int x, int y) {
1828        // we rely on the fact the View.scrollBy calls scrollTo.
1829        if (getChildCount() > 0) {
1830            View child = getChildAt(0);
1831            x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
1832            y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
1833            if (x != mScrollX || y != mScrollY) {
1834                super.scrollTo(x, y);
1835            }
1836        }
1837    }
1838
1839    private int clamp(int n, int my, int child) {
1840        if (my >= child || n < 0) {
1841            /* my >= child is this case:
1842             *                    |--------------- me ---------------|
1843             *     |------ child ------|
1844             * or
1845             *     |--------------- me ---------------|
1846             *            |------ child ------|
1847             * or
1848             *     |--------------- me ---------------|
1849             *                                  |------ child ------|
1850             *
1851             * n < 0 is this case:
1852             *     |------ me ------|
1853             *                    |-------- child --------|
1854             *     |-- mScrollX --|
1855             */
1856            return 0;
1857        }
1858        if ((my+n) > child) {
1859            /* this case:
1860             *                    |------ me ------|
1861             *     |------ child ------|
1862             *     |-- mScrollX --|
1863             */
1864            return child-my;
1865        }
1866        return n;
1867    }
1868
1869}
1870