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