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