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