Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.TypedArray;
     21 import android.graphics.Rect;
     22 import android.util.AttributeSet;
     23 import android.view.FocusFinder;
     24 import android.view.KeyEvent;
     25 import android.view.MotionEvent;
     26 import android.view.VelocityTracker;
     27 import android.view.View;
     28 import android.view.ViewConfiguration;
     29 import android.view.ViewGroup;
     30 import android.view.ViewParent;
     31 import android.view.animation.AnimationUtils;
     32 
     33 import java.util.List;
     34 
     35 /**
     36  * Layout container for a view hierarchy that can be scrolled by the user,
     37  * allowing it to be larger than the physical display.  A HorizontalScrollView
     38  * is a {@link FrameLayout}, meaning you should place one child in it
     39  * containing the entire contents to scroll; this child may itself be a layout
     40  * manager with a complex hierarchy of objects.  A child that is often used
     41  * is a {@link LinearLayout} in a horizontal orientation, presenting a horizontal
     42  * array of top-level items that the user can scroll through.
     43  *
     44  * <p>You should never use a HorizontalScrollView with a {@link ListView}, since
     45  * ListView takes care of its own scrolling.  Most importantly, doing this
     46  * defeats all of the important optimizations in ListView for dealing with
     47  * large lists, since it effectively forces the ListView to display its entire
     48  * list of items to fill up the infinite container supplied by HorizontalScrollView.
     49  *
     50  * <p>The {@link TextView} class also
     51  * takes care of its own scrolling, so does not require a ScrollView, but
     52  * using the two together is possible to achieve the effect of a text view
     53  * within a larger container.
     54  *
     55  * <p>HorizontalScrollView only supports horizontal scrolling.
     56  */
     57 public class HorizontalScrollView extends FrameLayout {
     58     private static final int ANIMATED_SCROLL_GAP = ScrollView.ANIMATED_SCROLL_GAP;
     59 
     60     private static final float MAX_SCROLL_FACTOR = ScrollView.MAX_SCROLL_FACTOR;
     61 
     62 
     63     private long mLastScroll;
     64 
     65     private final Rect mTempRect = new Rect();
     66     private Scroller mScroller;
     67 
     68     /**
     69      * Flag to indicate that we are moving focus ourselves. This is so the
     70      * code that watches for focus changes initiated outside this ScrollView
     71      * knows that it does not have to do anything.
     72      */
     73     private boolean mScrollViewMovedFocus;
     74 
     75     /**
     76      * Position of the last motion event.
     77      */
     78     private float mLastMotionX;
     79 
     80     /**
     81      * True when the layout has changed but the traversal has not come through yet.
     82      * Ideally the view hierarchy would keep track of this for us.
     83      */
     84     private boolean mIsLayoutDirty = true;
     85 
     86     /**
     87      * The child to give focus to in the event that a child has requested focus while the
     88      * layout is dirty. This prevents the scroll from being wrong if the child has not been
     89      * laid out before requesting focus.
     90      */
     91     private View mChildToScrollTo = null;
     92 
     93     /**
     94      * True if the user is currently dragging this ScrollView around. This is
     95      * not the same as 'is being flinged', which can be checked by
     96      * mScroller.isFinished() (flinging begins when the user lifts his finger).
     97      */
     98     private boolean mIsBeingDragged = false;
     99 
    100     /**
    101      * Determines speed during touch scrolling
    102      */
    103     private VelocityTracker mVelocityTracker;
    104 
    105     /**
    106      * When set to true, the scroll view measure its child to make it fill the currently
    107      * visible area.
    108      */
    109     private boolean mFillViewport;
    110 
    111     /**
    112      * Whether arrow scrolling is animated.
    113      */
    114     private boolean mSmoothScrollingEnabled = true;
    115 
    116     private int mTouchSlop;
    117     private int mMinimumVelocity;
    118     private int mMaximumVelocity;
    119 
    120     /**
    121      * ID of the active pointer. This is used to retain consistency during
    122      * drags/flings if multiple pointers are used.
    123      */
    124     private int mActivePointerId = INVALID_POINTER;
    125 
    126     /**
    127      * Sentinel value for no current active pointer.
    128      * Used by {@link #mActivePointerId}.
    129      */
    130     private static final int INVALID_POINTER = -1;
    131 
    132     public HorizontalScrollView(Context context) {
    133         this(context, null);
    134     }
    135 
    136     public HorizontalScrollView(Context context, AttributeSet attrs) {
    137         this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle);
    138     }
    139 
    140     public HorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
    141         super(context, attrs, defStyle);
    142         initScrollView();
    143 
    144         TypedArray a = context.obtainStyledAttributes(attrs,
    145                 android.R.styleable.HorizontalScrollView, defStyle, 0);
    146 
    147         setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false));
    148 
    149         a.recycle();
    150     }
    151 
    152     @Override
    153     protected float getLeftFadingEdgeStrength() {
    154         if (getChildCount() == 0) {
    155             return 0.0f;
    156         }
    157 
    158         final int length = getHorizontalFadingEdgeLength();
    159         if (mScrollX < length) {
    160             return mScrollX / (float) length;
    161         }
    162 
    163         return 1.0f;
    164     }
    165 
    166     @Override
    167     protected float getRightFadingEdgeStrength() {
    168         if (getChildCount() == 0) {
    169             return 0.0f;
    170         }
    171 
    172         final int length = getHorizontalFadingEdgeLength();
    173         final int rightEdge = getWidth() - mPaddingRight;
    174         final int span = getChildAt(0).getRight() - mScrollX - rightEdge;
    175         if (span < length) {
    176             return span / (float) length;
    177         }
    178 
    179         return 1.0f;
    180     }
    181 
    182     /**
    183      * @return The maximum amount this scroll view will scroll in response to
    184      *   an arrow event.
    185      */
    186     public int getMaxScrollAmount() {
    187         return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft));
    188     }
    189 
    190 
    191     private void initScrollView() {
    192         mScroller = new Scroller(getContext());
    193         setFocusable(true);
    194         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
    195         setWillNotDraw(false);
    196         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
    197         mTouchSlop = configuration.getScaledTouchSlop();
    198         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
    199         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    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 ScrollView's content is stretched to fill the viewport.
    252      *
    253      * @return True if the content fills the viewport, false otherwise.
    254      */
    255     public boolean isFillViewport() {
    256         return mFillViewport;
    257     }
    258 
    259     /**
    260      * Indicates this ScrollView whether it should stretch its content width to fill
    261      * the viewport or not.
    262      *
    263      * @param fillViewport True to stretch the content's width to the viewport's
    264      *        boundaries, false otherwise.
    265      */
    266     public void setFillViewport(boolean fillViewport) {
    267         if (fillViewport != mFillViewport) {
    268             mFillViewport = fillViewport;
    269             requestLayout();
    270         }
    271     }
    272 
    273     /**
    274      * @return Whether arrow scrolling will animate its transition.
    275      */
    276     public boolean isSmoothScrollingEnabled() {
    277         return mSmoothScrollingEnabled;
    278     }
    279 
    280     /**
    281      * Set whether arrow scrolling will animate its transition.
    282      * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
    283      */
    284     public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
    285         mSmoothScrollingEnabled = smoothScrollingEnabled;
    286     }
    287 
    288     @Override
    289     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    290         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    291 
    292         if (!mFillViewport) {
    293             return;
    294         }
    295 
    296         final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    297         if (widthMode == MeasureSpec.UNSPECIFIED) {
    298             return;
    299         }
    300 
    301         if (getChildCount() > 0) {
    302             final View child = getChildAt(0);
    303             int width = getMeasuredWidth();
    304             if (child.getMeasuredWidth() < width) {
    305                 final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
    306 
    307                 int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, mPaddingTop
    308                         + mPaddingBottom, lp.height);
    309                 width -= mPaddingLeft;
    310                 width -= mPaddingRight;
    311                 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
    312 
    313                 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    314             }
    315         }
    316     }
    317 
    318     @Override
    319     public boolean dispatchKeyEvent(KeyEvent event) {
    320         // Let the focused view and/or our descendants get the key first
    321         return super.dispatchKeyEvent(event) || executeKeyEvent(event);
    322     }
    323 
    324     /**
    325      * You can call this function yourself to have the scroll view perform
    326      * scrolling from a key event, just as if the event had been dispatched to
    327      * it by the view hierarchy.
    328      *
    329      * @param event The key event to execute.
    330      * @return Return true if the event was handled, else false.
    331      */
    332     public boolean executeKeyEvent(KeyEvent event) {
    333         mTempRect.setEmpty();
    334 
    335         if (!canScroll()) {
    336             if (isFocused()) {
    337                 View currentFocused = findFocus();
    338                 if (currentFocused == this) currentFocused = null;
    339                 View nextFocused = FocusFinder.getInstance().findNextFocus(this,
    340                         currentFocused, View.FOCUS_RIGHT);
    341                 return nextFocused != null && nextFocused != this &&
    342                         nextFocused.requestFocus(View.FOCUS_RIGHT);
    343             }
    344             return false;
    345         }
    346 
    347         boolean handled = false;
    348         if (event.getAction() == KeyEvent.ACTION_DOWN) {
    349             switch (event.getKeyCode()) {
    350                 case KeyEvent.KEYCODE_DPAD_LEFT:
    351                     if (!event.isAltPressed()) {
    352                         handled = arrowScroll(View.FOCUS_LEFT);
    353                     } else {
    354                         handled = fullScroll(View.FOCUS_LEFT);
    355                     }
    356                     break;
    357                 case KeyEvent.KEYCODE_DPAD_RIGHT:
    358                     if (!event.isAltPressed()) {
    359                         handled = arrowScroll(View.FOCUS_RIGHT);
    360                     } else {
    361                         handled = fullScroll(View.FOCUS_RIGHT);
    362                     }
    363                     break;
    364                 case KeyEvent.KEYCODE_SPACE:
    365                     pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
    366                     break;
    367             }
    368         }
    369 
    370         return handled;
    371     }
    372 
    373     private boolean inChild(int x, int y) {
    374         if (getChildCount() > 0) {
    375             final int scrollX = mScrollX;
    376             final View child = getChildAt(0);
    377             return !(y < child.getTop()
    378                     || y >= child.getBottom()
    379                     || x < child.getLeft() - scrollX
    380                     || x >= child.getRight() - scrollX);
    381         }
    382         return false;
    383     }
    384 
    385     @Override
    386     public boolean onInterceptTouchEvent(MotionEvent ev) {
    387         /*
    388          * This method JUST determines whether we want to intercept the motion.
    389          * If we return true, onMotionEvent will be called and we do the actual
    390          * scrolling there.
    391          */
    392 
    393         /*
    394         * Shortcut the most recurring case: the user is in the dragging
    395         * state and he is moving his finger.  We want to intercept this
    396         * motion.
    397         */
    398         final int action = ev.getAction();
    399         if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
    400             return true;
    401         }
    402 
    403         switch (action & MotionEvent.ACTION_MASK) {
    404             case MotionEvent.ACTION_MOVE: {
    405                 /*
    406                  * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
    407                  * whether the user has moved far enough from his original down touch.
    408                  */
    409 
    410                 /*
    411                 * Locally do absolute value. mLastMotionX is set to the x value
    412                 * of the down event.
    413                 */
    414                 final int activePointerId = mActivePointerId;
    415                 if (activePointerId == INVALID_POINTER) {
    416                     // If we don't have a valid id, the touch down wasn't on content.
    417                     break;
    418                 }
    419 
    420                 final int pointerIndex = ev.findPointerIndex(activePointerId);
    421                 final float x = ev.getX(pointerIndex);
    422                 final int xDiff = (int) Math.abs(x - mLastMotionX);
    423                 if (xDiff > mTouchSlop) {
    424                     mIsBeingDragged = true;
    425                     mLastMotionX = x;
    426                     if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
    427                 }
    428                 break;
    429             }
    430 
    431             case MotionEvent.ACTION_DOWN: {
    432                 final float x = ev.getX();
    433                 if (!inChild((int) x, (int) ev.getY())) {
    434                     mIsBeingDragged = false;
    435                     break;
    436                 }
    437 
    438                 /*
    439                  * Remember location of down touch.
    440                  * ACTION_DOWN always refers to pointer index 0.
    441                  */
    442                 mLastMotionX = x;
    443                 mActivePointerId = ev.getPointerId(0);
    444 
    445                 /*
    446                 * If being flinged and user touches the screen, initiate drag;
    447                 * otherwise don't.  mScroller.isFinished should be false when
    448                 * being flinged.
    449                 */
    450                 mIsBeingDragged = !mScroller.isFinished();
    451                 break;
    452             }
    453 
    454             case MotionEvent.ACTION_CANCEL:
    455             case MotionEvent.ACTION_UP:
    456                 /* Release the drag */
    457                 mIsBeingDragged = false;
    458                 mActivePointerId = INVALID_POINTER;
    459                 break;
    460             case MotionEvent.ACTION_POINTER_UP:
    461                 onSecondaryPointerUp(ev);
    462                 break;
    463         }
    464 
    465         /*
    466         * The only time we want to intercept motion events is if we are in the
    467         * drag mode.
    468         */
    469         return mIsBeingDragged;
    470     }
    471 
    472     @Override
    473     public boolean onTouchEvent(MotionEvent ev) {
    474 
    475         if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
    476             // Don't handle edge touches immediately -- they may actually belong to one of our
    477             // descendants.
    478             return false;
    479         }
    480 
    481         if (mVelocityTracker == null) {
    482             mVelocityTracker = VelocityTracker.obtain();
    483         }
    484         mVelocityTracker.addMovement(ev);
    485 
    486         final int action = ev.getAction();
    487 
    488         switch (action & MotionEvent.ACTION_MASK) {
    489             case MotionEvent.ACTION_DOWN: {
    490                 final float x = ev.getX();
    491                 if (!(mIsBeingDragged = inChild((int) x, (int) ev.getY()))) {
    492                     return false;
    493                 }
    494 
    495                 /*
    496                  * If being flinged and user touches, stop the fling. isFinished
    497                  * will be false if being flinged.
    498                  */
    499                 if (!mScroller.isFinished()) {
    500                     mScroller.abortAnimation();
    501                 }
    502 
    503                 // Remember where the motion event started
    504                 mLastMotionX = x;
    505                 mActivePointerId = ev.getPointerId(0);
    506                 break;
    507             }
    508             case MotionEvent.ACTION_MOVE:
    509                 if (mIsBeingDragged) {
    510                     // Scroll to follow the motion event
    511                     final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
    512                     final float x = ev.getX(activePointerIndex);
    513                     final int deltaX = (int) (mLastMotionX - x);
    514                     mLastMotionX = x;
    515 
    516                     scrollBy(deltaX, 0);
    517                 }
    518                 break;
    519             case MotionEvent.ACTION_UP:
    520                 if (mIsBeingDragged) {
    521                     final VelocityTracker velocityTracker = mVelocityTracker;
    522                     velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
    523                     int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
    524 
    525                     if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) {
    526                         fling(-initialVelocity);
    527                     }
    528 
    529                     mActivePointerId = INVALID_POINTER;
    530                     mIsBeingDragged = false;
    531 
    532                     if (mVelocityTracker != null) {
    533                         mVelocityTracker.recycle();
    534                         mVelocityTracker = null;
    535                     }
    536                 }
    537                 break;
    538             case MotionEvent.ACTION_CANCEL:
    539                 if (mIsBeingDragged && getChildCount() > 0) {
    540                     mActivePointerId = INVALID_POINTER;
    541                     mIsBeingDragged = false;
    542                     if (mVelocityTracker != null) {
    543                         mVelocityTracker.recycle();
    544                         mVelocityTracker = null;
    545                     }
    546                 }
    547                 break;
    548             case MotionEvent.ACTION_POINTER_UP:
    549                 onSecondaryPointerUp(ev);
    550                 break;
    551         }
    552         return true;
    553     }
    554 
    555     private void onSecondaryPointerUp(MotionEvent ev) {
    556         final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
    557                 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
    558         final int pointerId = ev.getPointerId(pointerIndex);
    559         if (pointerId == mActivePointerId) {
    560             // This was our active pointer going up. Choose a new
    561             // active pointer and adjust accordingly.
    562             // TODO: Make this decision more intelligent.
    563             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
    564             mLastMotionX = ev.getX(newPointerIndex);
    565             mActivePointerId = ev.getPointerId(newPointerIndex);
    566             if (mVelocityTracker != null) {
    567                 mVelocityTracker.clear();
    568             }
    569         }
    570     }
    571 
    572     private int getScrollRange() {
    573         int scrollRange = 0;
    574         if (getChildCount() > 0) {
    575             View child = getChildAt(0);
    576             scrollRange = Math.max(0,
    577                     child.getWidth() - getWidth() - mPaddingLeft - mPaddingRight);
    578         }
    579         return scrollRange;
    580     }
    581 
    582     /**
    583      * <p>
    584      * Finds the next focusable component that fits in this View's bounds
    585      * (excluding fading edges) pretending that this View's left is located at
    586      * the parameter left.
    587      * </p>
    588      *
    589      * @param leftFocus          look for a candidate is the one at the left of the bounds
    590      *                           if leftFocus is true, or at the right of the bounds if leftFocus
    591      *                           is false
    592      * @param left               the left offset of the bounds in which a focusable must be
    593      *                           found (the fading edge is assumed to start at this position)
    594      * @param preferredFocusable the View that has highest priority and will be
    595      *                           returned if it is within my bounds (null is valid)
    596      * @return the next focusable component in the bounds or null if none can be found
    597      */
    598     private View findFocusableViewInMyBounds(final boolean leftFocus,
    599             final int left, View preferredFocusable) {
    600         /*
    601          * The fading edge's transparent side should be considered for focus
    602          * since it's mostly visible, so we divide the actual fading edge length
    603          * by 2.
    604          */
    605         final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
    606         final int leftWithoutFadingEdge = left + fadingEdgeLength;
    607         final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
    608 
    609         if ((preferredFocusable != null)
    610                 && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
    611                 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
    612             return preferredFocusable;
    613         }
    614 
    615         return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
    616                 rightWithoutFadingEdge);
    617     }
    618 
    619     /**
    620      * <p>
    621      * Finds the next focusable component that fits in the specified bounds.
    622      * </p>
    623      *
    624      * @param leftFocus look for a candidate is the one at the left of the bounds
    625      *                  if leftFocus is true, or at the right of the bounds if
    626      *                  leftFocus is false
    627      * @param left      the left offset of the bounds in which a focusable must be
    628      *                  found
    629      * @param right     the right offset of the bounds in which a focusable must
    630      *                  be found
    631      * @return the next focusable component in the bounds or null if none can
    632      *         be found
    633      */
    634     private View findFocusableViewInBounds(boolean leftFocus, int left, int right) {
    635 
    636         List<View> focusables = getFocusables(View.FOCUS_FORWARD);
    637         View focusCandidate = null;
    638 
    639         /*
    640          * A fully contained focusable is one where its left is below the bound's
    641          * left, and its right is above the bound's right. A partially
    642          * contained focusable is one where some part of it is within the
    643          * bounds, but it also has some part that is not within bounds.  A fully contained
    644          * focusable is preferred to a partially contained focusable.
    645          */
    646         boolean foundFullyContainedFocusable = false;
    647 
    648         int count = focusables.size();
    649         for (int i = 0; i < count; i++) {
    650             View view = focusables.get(i);
    651             int viewLeft = view.getLeft();
    652             int viewRight = view.getRight();
    653 
    654             if (left < viewRight && viewLeft < right) {
    655                 /*
    656                  * the focusable is in the target area, it is a candidate for
    657                  * focusing
    658                  */
    659 
    660                 final boolean viewIsFullyContained = (left < viewLeft) &&
    661                         (viewRight < right);
    662 
    663                 if (focusCandidate == null) {
    664                     /* No candidate, take this one */
    665                     focusCandidate = view;
    666                     foundFullyContainedFocusable = viewIsFullyContained;
    667                 } else {
    668                     final boolean viewIsCloserToBoundary =
    669                             (leftFocus && viewLeft < focusCandidate.getLeft()) ||
    670                                     (!leftFocus && viewRight > focusCandidate.getRight());
    671 
    672                     if (foundFullyContainedFocusable) {
    673                         if (viewIsFullyContained && viewIsCloserToBoundary) {
    674                             /*
    675                              * We're dealing with only fully contained views, so
    676                              * it has to be closer to the boundary to beat our
    677                              * candidate
    678                              */
    679                             focusCandidate = view;
    680                         }
    681                     } else {
    682                         if (viewIsFullyContained) {
    683                             /* Any fully contained view beats a partially contained view */
    684                             focusCandidate = view;
    685                             foundFullyContainedFocusable = true;
    686                         } else if (viewIsCloserToBoundary) {
    687                             /*
    688                              * Partially contained view beats another partially
    689                              * contained view if it's closer
    690                              */
    691                             focusCandidate = view;
    692                         }
    693                     }
    694                 }
    695             }
    696         }
    697 
    698         return focusCandidate;
    699     }
    700 
    701     /**
    702      * <p>Handles scrolling in response to a "page up/down" shortcut press. This
    703      * method will scroll the view by one page left or right and give the focus
    704      * to the leftmost/rightmost component in the new visible area. If no
    705      * component is a good candidate for focus, this scrollview reclaims the
    706      * focus.</p>
    707      *
    708      * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
    709      *                  to go one page left or {@link android.view.View#FOCUS_RIGHT}
    710      *                  to go one page right
    711      * @return true if the key event is consumed by this method, false otherwise
    712      */
    713     public boolean pageScroll(int direction) {
    714         boolean right = direction == View.FOCUS_RIGHT;
    715         int width = getWidth();
    716 
    717         if (right) {
    718             mTempRect.left = getScrollX() + width;
    719             int count = getChildCount();
    720             if (count > 0) {
    721                 View view = getChildAt(0);
    722                 if (mTempRect.left + width > view.getRight()) {
    723                     mTempRect.left = view.getRight() - width;
    724                 }
    725             }
    726         } else {
    727             mTempRect.left = getScrollX() - width;
    728             if (mTempRect.left < 0) {
    729                 mTempRect.left = 0;
    730             }
    731         }
    732         mTempRect.right = mTempRect.left + width;
    733 
    734         return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
    735     }
    736 
    737     /**
    738      * <p>Handles scrolling in response to a "home/end" shortcut press. This
    739      * method will scroll the view to the left or right and give the focus
    740      * to the leftmost/rightmost component in the new visible area. If no
    741      * component is a good candidate for focus, this scrollview reclaims the
    742      * focus.</p>
    743      *
    744      * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
    745      *                  to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
    746      *                  to go the right
    747      * @return true if the key event is consumed by this method, false otherwise
    748      */
    749     public boolean fullScroll(int direction) {
    750         boolean right = direction == View.FOCUS_RIGHT;
    751         int width = getWidth();
    752 
    753         mTempRect.left = 0;
    754         mTempRect.right = width;
    755 
    756         if (right) {
    757             int count = getChildCount();
    758             if (count > 0) {
    759                 View view = getChildAt(0);
    760                 mTempRect.right = view.getRight();
    761                 mTempRect.left = mTempRect.right - width;
    762             }
    763         }
    764 
    765         return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
    766     }
    767 
    768     /**
    769      * <p>Scrolls the view to make the area defined by <code>left</code> and
    770      * <code>right</code> visible. This method attempts to give the focus
    771      * to a component visible in this area. If no component can be focused in
    772      * the new visible area, the focus is reclaimed by this scrollview.</p>
    773      *
    774      * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
    775      *                  to go left {@link android.view.View#FOCUS_RIGHT} to right
    776      * @param left     the left offset of the new area to be made visible
    777      * @param right    the right offset of the new area to be made visible
    778      * @return true if the key event is consumed by this method, false otherwise
    779      */
    780     private boolean scrollAndFocus(int direction, int left, int right) {
    781         boolean handled = true;
    782 
    783         int width = getWidth();
    784         int containerLeft = getScrollX();
    785         int containerRight = containerLeft + width;
    786         boolean goLeft = direction == View.FOCUS_LEFT;
    787 
    788         View newFocused = findFocusableViewInBounds(goLeft, left, right);
    789         if (newFocused == null) {
    790             newFocused = this;
    791         }
    792 
    793         if (left >= containerLeft && right <= containerRight) {
    794             handled = false;
    795         } else {
    796             int delta = goLeft ? (left - containerLeft) : (right - containerRight);
    797             doScrollX(delta);
    798         }
    799 
    800         if (newFocused != findFocus() && newFocused.requestFocus(direction)) {
    801             mScrollViewMovedFocus = true;
    802             mScrollViewMovedFocus = false;
    803         }
    804 
    805         return handled;
    806     }
    807 
    808     /**
    809      * Handle scrolling in response to a left or right arrow click.
    810      *
    811      * @param direction The direction corresponding to the arrow key that was
    812      *                  pressed
    813      * @return True if we consumed the event, false otherwise
    814      */
    815     public boolean arrowScroll(int direction) {
    816 
    817         View currentFocused = findFocus();
    818         if (currentFocused == this) currentFocused = null;
    819 
    820         View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
    821 
    822         final int maxJump = getMaxScrollAmount();
    823 
    824         if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
    825             nextFocused.getDrawingRect(mTempRect);
    826             offsetDescendantRectToMyCoords(nextFocused, mTempRect);
    827             int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
    828             doScrollX(scrollDelta);
    829             nextFocused.requestFocus(direction);
    830         } else {
    831             // no new focus
    832             int scrollDelta = maxJump;
    833 
    834             if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
    835                 scrollDelta = getScrollX();
    836             } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) {
    837 
    838                 int daRight = getChildAt(0).getRight();
    839 
    840                 int screenRight = getScrollX() + getWidth();
    841 
    842                 if (daRight - screenRight < maxJump) {
    843                     scrollDelta = daRight - screenRight;
    844                 }
    845             }
    846             if (scrollDelta == 0) {
    847                 return false;
    848             }
    849             doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta);
    850         }
    851 
    852         if (currentFocused != null && currentFocused.isFocused()
    853                 && isOffScreen(currentFocused)) {
    854             // previously focused item still has focus and is off screen, give
    855             // it up (take it back to ourselves)
    856             // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
    857             // sure to
    858             // get it)
    859             final int descendantFocusability = getDescendantFocusability();  // save
    860             setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
    861             requestFocus();
    862             setDescendantFocusability(descendantFocusability);  // restore
    863         }
    864         return true;
    865     }
    866 
    867     /**
    868      * @return whether the descendant of this scroll view is scrolled off
    869      *  screen.
    870      */
    871     private boolean isOffScreen(View descendant) {
    872         return !isWithinDeltaOfScreen(descendant, 0);
    873     }
    874 
    875     /**
    876      * @return whether the descendant of this scroll view is within delta
    877      *  pixels of being on the screen.
    878      */
    879     private boolean isWithinDeltaOfScreen(View descendant, int delta) {
    880         descendant.getDrawingRect(mTempRect);
    881         offsetDescendantRectToMyCoords(descendant, mTempRect);
    882 
    883         return (mTempRect.right + delta) >= getScrollX()
    884                 && (mTempRect.left - delta) <= (getScrollX() + getWidth());
    885     }
    886 
    887     /**
    888      * Smooth scroll by a X delta
    889      *
    890      * @param delta the number of pixels to scroll by on the X axis
    891      */
    892     private void doScrollX(int delta) {
    893         if (delta != 0) {
    894             if (mSmoothScrollingEnabled) {
    895                 smoothScrollBy(delta, 0);
    896             } else {
    897                 scrollBy(delta, 0);
    898             }
    899         }
    900     }
    901 
    902     /**
    903      * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
    904      *
    905      * @param dx the number of pixels to scroll by on the X axis
    906      * @param dy the number of pixels to scroll by on the Y axis
    907      */
    908     public final void smoothScrollBy(int dx, int dy) {
    909         if (getChildCount() == 0) {
    910             // Nothing to do.
    911             return;
    912         }
    913         long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
    914         if (duration > ANIMATED_SCROLL_GAP) {
    915             final int width = getWidth() - mPaddingRight - mPaddingLeft;
    916             final int right = getChildAt(0).getWidth();
    917             final int maxX = Math.max(0, right - width);
    918             final int scrollX = mScrollX;
    919             dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
    920 
    921             mScroller.startScroll(scrollX, mScrollY, dx, 0);
    922             invalidate();
    923         } else {
    924             if (!mScroller.isFinished()) {
    925                 mScroller.abortAnimation();
    926             }
    927             scrollBy(dx, dy);
    928         }
    929         mLastScroll = AnimationUtils.currentAnimationTimeMillis();
    930     }
    931 
    932     /**
    933      * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
    934      *
    935      * @param x the position where to scroll on the X axis
    936      * @param y the position where to scroll on the Y axis
    937      */
    938     public final void smoothScrollTo(int x, int y) {
    939         smoothScrollBy(x - mScrollX, y - mScrollY);
    940     }
    941 
    942     /**
    943      * <p>The scroll range of a scroll view is the overall width of all of its
    944      * children.</p>
    945      */
    946     @Override
    947     protected int computeHorizontalScrollRange() {
    948         final int count = getChildCount();
    949         final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight;
    950         if (count == 0) {
    951             return contentWidth;
    952         }
    953 
    954         return getChildAt(0).getRight();
    955     }
    956 
    957     @Override
    958     protected int computeHorizontalScrollOffset() {
    959         return Math.max(0, super.computeHorizontalScrollOffset());
    960     }
    961 
    962     @Override
    963     protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
    964         ViewGroup.LayoutParams lp = child.getLayoutParams();
    965 
    966         int childWidthMeasureSpec;
    967         int childHeightMeasureSpec;
    968 
    969         childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop
    970                 + mPaddingBottom, lp.height);
    971 
    972         childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    973 
    974         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    975     }
    976 
    977     @Override
    978     protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
    979             int parentHeightMeasureSpec, int heightUsed) {
    980         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    981 
    982         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
    983                 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
    984                         + heightUsed, lp.height);
    985         final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
    986                 lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
    987 
    988         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    989     }
    990 
    991     @Override
    992     public void computeScroll() {
    993         if (mScroller.computeScrollOffset()) {
    994             // This is called at drawing time by ViewGroup.  We don't want to
    995             // re-show the scrollbars at this point, which scrollTo will do,
    996             // so we replicate most of scrollTo here.
    997             //
    998             //         It's a little odd to call onScrollChanged from inside the drawing.
    999             //
   1000             //         It is, except when you remember that computeScroll() is used to
   1001             //         animate scrolling. So unless we want to defer the onScrollChanged()
   1002             //         until the end of the animated scrolling, we don't really have a
   1003             //         choice here.
   1004             //
   1005             //         I agree.  The alternative, which I think would be worse, is to post
   1006             //         something and tell the subclasses later.  This is bad because there
   1007             //         will be a window where mScrollX/Y is different from what the app
   1008             //         thinks it is.
   1009             //
   1010             int oldX = mScrollX;
   1011             int oldY = mScrollY;
   1012             int x = mScroller.getCurrX();
   1013             int y = mScroller.getCurrY();
   1014 
   1015             if (getChildCount() > 0) {
   1016                 View child = getChildAt(0);
   1017                 x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
   1018                 y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
   1019                 if (x != oldX || y != oldY) {
   1020                     mScrollX = x;
   1021                     mScrollY = y;
   1022                     onScrollChanged(x, y, oldX, oldY);
   1023                 }
   1024             }
   1025             awakenScrollBars();
   1026 
   1027             // Keep on drawing until the animation has finished.
   1028             postInvalidate();
   1029         }
   1030     }
   1031 
   1032     /**
   1033      * Scrolls the view to the given child.
   1034      *
   1035      * @param child the View to scroll to
   1036      */
   1037     private void scrollToChild(View child) {
   1038         child.getDrawingRect(mTempRect);
   1039 
   1040         /* Offset from child's local coordinates to ScrollView coordinates */
   1041         offsetDescendantRectToMyCoords(child, mTempRect);
   1042 
   1043         int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
   1044 
   1045         if (scrollDelta != 0) {
   1046             scrollBy(scrollDelta, 0);
   1047         }
   1048     }
   1049 
   1050     /**
   1051      * If rect is off screen, scroll just enough to get it (or at least the
   1052      * first screen size chunk of it) on screen.
   1053      *
   1054      * @param rect      The rectangle.
   1055      * @param immediate True to scroll immediately without animation
   1056      * @return true if scrolling was performed
   1057      */
   1058     private boolean scrollToChildRect(Rect rect, boolean immediate) {
   1059         final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
   1060         final boolean scroll = delta != 0;
   1061         if (scroll) {
   1062             if (immediate) {
   1063                 scrollBy(delta, 0);
   1064             } else {
   1065                 smoothScrollBy(delta, 0);
   1066             }
   1067         }
   1068         return scroll;
   1069     }
   1070 
   1071     /**
   1072      * Compute the amount to scroll in the X direction in order to get
   1073      * a rectangle completely on the screen (or, if taller than the screen,
   1074      * at least the first screen size chunk of it).
   1075      *
   1076      * @param rect The rect.
   1077      * @return The scroll delta.
   1078      */
   1079     protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
   1080         if (getChildCount() == 0) return 0;
   1081 
   1082         int width = getWidth();
   1083         int screenLeft = getScrollX();
   1084         int screenRight = screenLeft + width;
   1085 
   1086         int fadingEdge = getHorizontalFadingEdgeLength();
   1087 
   1088         // leave room for left fading edge as long as rect isn't at very left
   1089         if (rect.left > 0) {
   1090             screenLeft += fadingEdge;
   1091         }
   1092 
   1093         // leave room for right fading edge as long as rect isn't at very right
   1094         if (rect.right < getChildAt(0).getWidth()) {
   1095             screenRight -= fadingEdge;
   1096         }
   1097 
   1098         int scrollXDelta = 0;
   1099 
   1100         if (rect.right > screenRight && rect.left > screenLeft) {
   1101             // need to move right to get it in view: move right just enough so
   1102             // that the entire rectangle is in view (or at least the first
   1103             // screen size chunk).
   1104 
   1105             if (rect.width() > width) {
   1106                 // just enough to get screen size chunk on
   1107                 scrollXDelta += (rect.left - screenLeft);
   1108             } else {
   1109                 // get entire rect at right of screen
   1110                 scrollXDelta += (rect.right - screenRight);
   1111             }
   1112 
   1113             // make sure we aren't scrolling beyond the end of our content
   1114             int right = getChildAt(0).getRight();
   1115             int distanceToRight = right - screenRight;
   1116             scrollXDelta = Math.min(scrollXDelta, distanceToRight);
   1117 
   1118         } else if (rect.left < screenLeft && rect.right < screenRight) {
   1119             // need to move right to get it in view: move right just enough so that
   1120             // entire rectangle is in view (or at least the first screen
   1121             // size chunk of it).
   1122 
   1123             if (rect.width() > width) {
   1124                 // screen size chunk
   1125                 scrollXDelta -= (screenRight - rect.right);
   1126             } else {
   1127                 // entire rect at left
   1128                 scrollXDelta -= (screenLeft - rect.left);
   1129             }
   1130 
   1131             // make sure we aren't scrolling any further than the left our content
   1132             scrollXDelta = Math.max(scrollXDelta, -getScrollX());
   1133         }
   1134         return scrollXDelta;
   1135     }
   1136 
   1137     @Override
   1138     public void requestChildFocus(View child, View focused) {
   1139         if (!mScrollViewMovedFocus) {
   1140             if (!mIsLayoutDirty) {
   1141                 scrollToChild(focused);
   1142             } else {
   1143                 // The child may not be laid out yet, we can't compute the scroll yet
   1144                 mChildToScrollTo = focused;
   1145             }
   1146         }
   1147         super.requestChildFocus(child, focused);
   1148     }
   1149 
   1150 
   1151     /**
   1152      * When looking for focus in children of a scroll view, need to be a little
   1153      * more careful not to give focus to something that is scrolled off screen.
   1154      *
   1155      * This is more expensive than the default {@link android.view.ViewGroup}
   1156      * implementation, otherwise this behavior might have been made the default.
   1157      */
   1158     @Override
   1159     protected boolean onRequestFocusInDescendants(int direction,
   1160             Rect previouslyFocusedRect) {
   1161 
   1162         // convert from forward / backward notation to up / down / left / right
   1163         // (ugh).
   1164         if (direction == View.FOCUS_FORWARD) {
   1165             direction = View.FOCUS_RIGHT;
   1166         } else if (direction == View.FOCUS_BACKWARD) {
   1167             direction = View.FOCUS_LEFT;
   1168         }
   1169 
   1170         final View nextFocus = previouslyFocusedRect == null ?
   1171                 FocusFinder.getInstance().findNextFocus(this, null, direction) :
   1172                 FocusFinder.getInstance().findNextFocusFromRect(this,
   1173                         previouslyFocusedRect, direction);
   1174 
   1175         if (nextFocus == null) {
   1176             return false;
   1177         }
   1178 
   1179         if (isOffScreen(nextFocus)) {
   1180             return false;
   1181         }
   1182 
   1183         return nextFocus.requestFocus(direction, previouslyFocusedRect);
   1184     }
   1185 
   1186     @Override
   1187     public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
   1188             boolean immediate) {
   1189         // offset into coordinate space of this scroll view
   1190         rectangle.offset(child.getLeft() - child.getScrollX(),
   1191                 child.getTop() - child.getScrollY());
   1192 
   1193         return scrollToChildRect(rectangle, immediate);
   1194     }
   1195 
   1196     @Override
   1197     public void requestLayout() {
   1198         mIsLayoutDirty = true;
   1199         super.requestLayout();
   1200     }
   1201 
   1202     @Override
   1203     protected void onLayout(boolean changed, int l, int t, int r, int b) {
   1204         super.onLayout(changed, l, t, r, b);
   1205         mIsLayoutDirty = false;
   1206         // Give a child focus if it needs it
   1207         if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
   1208                 scrollToChild(mChildToScrollTo);
   1209         }
   1210         mChildToScrollTo = null;
   1211 
   1212         // Calling this with the present values causes it to re-clam them
   1213         scrollTo(mScrollX, mScrollY);
   1214     }
   1215 
   1216     @Override
   1217     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
   1218         super.onSizeChanged(w, h, oldw, oldh);
   1219 
   1220         View currentFocused = findFocus();
   1221         if (null == currentFocused || this == currentFocused)
   1222             return;
   1223 
   1224         final int maxJump = mRight - mLeft;
   1225 
   1226         if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
   1227             currentFocused.getDrawingRect(mTempRect);
   1228             offsetDescendantRectToMyCoords(currentFocused, mTempRect);
   1229             int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
   1230             doScrollX(scrollDelta);
   1231         }
   1232     }
   1233 
   1234     /**
   1235      * Return true if child is an descendant of parent, (or equal to the parent).
   1236      */
   1237     private boolean isViewDescendantOf(View child, View parent) {
   1238         if (child == parent) {
   1239             return true;
   1240         }
   1241 
   1242         final ViewParent theParent = child.getParent();
   1243         return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
   1244     }
   1245 
   1246     /**
   1247      * Fling the scroll view
   1248      *
   1249      * @param velocityX The initial velocity in the X direction. Positive
   1250      *                  numbers mean that the finger/curor is moving down the screen,
   1251      *                  which means we want to scroll towards the left.
   1252      */
   1253     public void fling(int velocityX) {
   1254         if (getChildCount() > 0) {
   1255             int width = getWidth() - mPaddingRight - mPaddingLeft;
   1256             int right = getChildAt(0).getWidth();
   1257 
   1258             mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
   1259                     Math.max(0, right - width), 0, 0);
   1260 
   1261             final boolean movingRight = velocityX > 0;
   1262 
   1263             View newFocused = findFocusableViewInMyBounds(movingRight,
   1264                     mScroller.getFinalX(), findFocus());
   1265 
   1266             if (newFocused == null) {
   1267                 newFocused = this;
   1268             }
   1269 
   1270             if (newFocused != findFocus()
   1271                     && newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT)) {
   1272                 mScrollViewMovedFocus = true;
   1273                 mScrollViewMovedFocus = false;
   1274             }
   1275 
   1276             invalidate();
   1277         }
   1278     }
   1279 
   1280     /**
   1281      * {@inheritDoc}
   1282      *
   1283      * <p>This version also clamps the scrolling to the bounds of our child.
   1284      */
   1285     public void scrollTo(int x, int y) {
   1286         // we rely on the fact the View.scrollBy calls scrollTo.
   1287         if (getChildCount() > 0) {
   1288             View child = getChildAt(0);
   1289             x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
   1290             y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
   1291             if (x != mScrollX || y != mScrollY) {
   1292                 super.scrollTo(x, y);
   1293             }
   1294         }
   1295     }
   1296 
   1297     private int clamp(int n, int my, int child) {
   1298         if (my >= child || n < 0) {
   1299             return 0;
   1300         }
   1301         if ((my + n) > child) {
   1302             return child - my;
   1303         }
   1304         return n;
   1305     }
   1306 }
   1307