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