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