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