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