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