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