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