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