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