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