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