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