Home | History | Annotate | Download | only in view
      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 
     17 package com.android.browser.view;
     18 
     19 import android.content.Context;
     20 import android.content.res.TypedArray;
     21 import android.graphics.PointF;
     22 import android.graphics.Rect;
     23 import android.os.StrictMode;
     24 import android.util.AttributeSet;
     25 import android.view.FocusFinder;
     26 import android.view.InputDevice;
     27 import android.view.KeyEvent;
     28 import android.view.MotionEvent;
     29 import android.view.VelocityTracker;
     30 import android.view.View;
     31 import android.view.ViewConfiguration;
     32 import android.view.ViewDebug;
     33 import android.view.ViewGroup;
     34 import android.view.ViewParent;
     35 import android.view.accessibility.AccessibilityEvent;
     36 import android.view.accessibility.AccessibilityNodeInfo;
     37 import android.view.animation.AnimationUtils;
     38 import android.widget.FrameLayout;
     39 import android.widget.LinearLayout;
     40 import android.widget.OverScroller;
     41 import android.widget.TextView;
     42 
     43 import com.android.internal.R;
     44 
     45 import 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  */
     65 public 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