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