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