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