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