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
17package android.widget;
18
19import android.annotation.NonNull;
20import android.content.Context;
21import android.content.res.Configuration;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.Rect;
25import android.os.Build;
26import android.os.Bundle;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.view.FocusFinder;
32import android.view.InputDevice;
33import android.view.KeyEvent;
34import android.view.MotionEvent;
35import android.view.VelocityTracker;
36import android.view.View;
37import android.view.ViewConfiguration;
38import android.view.ViewDebug;
39import android.view.ViewGroup;
40import android.view.ViewHierarchyEncoder;
41import android.view.ViewParent;
42import android.view.accessibility.AccessibilityEvent;
43import android.view.accessibility.AccessibilityNodeInfo;
44import android.view.animation.AnimationUtils;
45
46import com.android.internal.R;
47
48import 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 */
69public 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
148    public HorizontalScrollView(Context context) {
149        this(context, null);
150    }
151
152    public HorizontalScrollView(Context context, AttributeSet attrs) {
153        this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle);
154    }
155
156    public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
157        this(context, attrs, defStyleAttr, 0);
158    }
159
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
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
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     */
211    public int getMaxScrollAmount() {
212        return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft));
213    }
214
215
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
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
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
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
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     */
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     */
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     */
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     */
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     */
317    public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
318        mSmoothScrollingEnabled = smoothScrollingEnabled;
319    }
320
321    @Override
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
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     */
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
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
426    private void initOrResetVelocityTracker() {
427        if (mVelocityTracker == null) {
428            mVelocityTracker = VelocityTracker.obtain();
429        } else {
430            mVelocityTracker.clear();
431        }
432    }
433
434    private void initVelocityTrackerIfNotExists() {
435        if (mVelocityTracker == null) {
436            mVelocityTracker = VelocityTracker.obtain();
437        }
438    }
439
440    private void recycleVelocityTracker() {
441        if (mVelocityTracker != null) {
442            mVelocityTracker.recycle();
443            mVelocityTracker = null;
444        }
445    }
446
447    @Override
448    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
449        if (disallowIntercept) {
450            recycleVelocityTracker();
451        }
452        super.requestDisallowInterceptTouchEvent(disallowIntercept);
453    }
454
455    @Override
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
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
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
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
768    public boolean shouldDelayChildPressedState() {
769        return true;
770    }
771
772    @Override
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
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
829    public CharSequence getAccessibilityClassName() {
830        return HorizontalScrollView.class.getName();
831    }
832
833    /** @hide */
834    @Override
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
1254    protected int computeHorizontalScrollOffset() {
1255        return Math.max(0, super.computeHorizontalScrollOffset());
1256    }
1257
1258    @Override
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
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
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     */
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     */
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     */
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
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
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
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
1504    public void requestLayout() {
1505        mIsLayoutDirty = true;
1506        super.requestLayout();
1507    }
1508
1509    @Override
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
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     */
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     */
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
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
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
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
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
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
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
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
1737        SavedState(Parcelable superState) {
1738            super(superState);
1739        }
1740
1741        public SavedState(Parcel source) {
1742            super(source);
1743            scrollOffsetFromStart = source.readInt();
1744        }
1745
1746        @Override
1747        public void writeToParcel(Parcel dest, int flags) {
1748            super.writeToParcel(dest, flags);
1749            dest.writeInt(scrollOffsetFromStart);
1750        }
1751
1752        @Override
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