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