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