• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import com.android.internal.R;
20 
21 import android.util.AttributeSet;
22 import android.graphics.Canvas;
23 import android.graphics.Rect;
24 import android.graphics.drawable.Drawable;
25 import android.view.View;
26 import android.view.VelocityTracker;
27 import android.view.ViewConfiguration;
28 import android.view.ViewGroup;
29 import android.view.KeyEvent;
30 import android.view.FocusFinder;
31 import android.view.MotionEvent;
32 import android.view.ViewParent;
33 import android.view.animation.AnimationUtils;
34 import android.content.Context;
35 import android.content.res.Resources;
36 import android.content.res.TypedArray;
37 
38 import java.util.List;
39 
40 /**
41  * Layout container for a view hierarchy that can be scrolled by the user,
42  * allowing it to be larger than the physical display.  A HorizontalScrollView
43  * is a {@link FrameLayout}, meaning you should place one child in it
44  * containing the entire contents to scroll; this child may itself be a layout
45  * manager with a complex hierarchy of objects.  A child that is often used
46  * is a {@link LinearLayout} in a horizontal orientation, presenting a horizontal
47  * array of top-level items that the user can scroll through.
48  *
49  * <p>You should never use a HorizontalScrollView with a {@link ListView}, since
50  * ListView takes care of its own scrolling.  Most importantly, doing this
51  * defeats all of the important optimizations in ListView for dealing with
52  * large lists, since it effectively forces the ListView to display its entire
53  * list of items to fill up the infinite container supplied by HorizontalScrollView.
54  *
55  * <p>The {@link TextView} class also
56  * takes care of its own scrolling, so does not require a ScrollView, but
57  * using the two together is possible to achieve the effect of a text view
58  * within a larger container.
59  *
60  * <p>HorizontalScrollView only supports horizontal scrolling.
61  */
62 public class HorizontalScrollView extends FrameLayout {
63     private static final int ANIMATED_SCROLL_GAP = ScrollView.ANIMATED_SCROLL_GAP;
64 
65     private static final float MAX_SCROLL_FACTOR = ScrollView.MAX_SCROLL_FACTOR;
66 
67 
68     private long mLastScroll;
69 
70     private final Rect mTempRect = new Rect();
71     private OverScroller mScroller;
72     private EdgeGlow mEdgeGlowLeft;
73     private EdgeGlow mEdgeGlowRight;
74 
75     /**
76      * Flag to indicate that we are moving focus ourselves. This is so the
77      * code that watches for focus changes initiated outside this ScrollView
78      * knows that it does not have to do anything.
79      */
80     private boolean mScrollViewMovedFocus;
81 
82     /**
83      * Position of the last motion event.
84      */
85     private float 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     private boolean mFillViewport;
117 
118     /**
119      * Whether arrow scrolling is animated.
120      */
121     private boolean mSmoothScrollingEnabled = true;
122 
123     private int mTouchSlop;
124     private int mMinimumVelocity;
125     private int mMaximumVelocity;
126 
127     private int mOverscrollDistance;
128     private int mOverflingDistance;
129 
130     /**
131      * ID of the active pointer. This is used to retain consistency during
132      * drags/flings if multiple pointers are used.
133      */
134     private int mActivePointerId = INVALID_POINTER;
135 
136     /**
137      * Sentinel value for no current active pointer.
138      * Used by {@link #mActivePointerId}.
139      */
140     private static final int INVALID_POINTER = -1;
141 
HorizontalScrollView(Context context)142     public HorizontalScrollView(Context context) {
143         this(context, null);
144     }
145 
HorizontalScrollView(Context context, AttributeSet attrs)146     public HorizontalScrollView(Context context, AttributeSet attrs) {
147         this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle);
148     }
149 
HorizontalScrollView(Context context, AttributeSet attrs, int defStyle)150     public HorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
151         super(context, attrs, defStyle);
152         initScrollView();
153 
154         TypedArray a = context.obtainStyledAttributes(attrs,
155                 android.R.styleable.HorizontalScrollView, defStyle, 0);
156 
157         setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false));
158 
159         a.recycle();
160     }
161 
162     @Override
getLeftFadingEdgeStrength()163     protected float getLeftFadingEdgeStrength() {
164         if (getChildCount() == 0) {
165             return 0.0f;
166         }
167 
168         final int length = getHorizontalFadingEdgeLength();
169         if (mScrollX < length) {
170             return mScrollX / (float) length;
171         }
172 
173         return 1.0f;
174     }
175 
176     @Override
getRightFadingEdgeStrength()177     protected float getRightFadingEdgeStrength() {
178         if (getChildCount() == 0) {
179             return 0.0f;
180         }
181 
182         final int length = getHorizontalFadingEdgeLength();
183         final int rightEdge = getWidth() - mPaddingRight;
184         final int span = getChildAt(0).getRight() - mScrollX - rightEdge;
185         if (span < length) {
186             return span / (float) length;
187         }
188 
189         return 1.0f;
190     }
191 
192     /**
193      * @return The maximum amount this scroll view will scroll in response to
194      *   an arrow event.
195      */
getMaxScrollAmount()196     public int getMaxScrollAmount() {
197         return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft));
198     }
199 
200 
initScrollView()201     private void initScrollView() {
202         mScroller = new OverScroller(getContext());
203         setFocusable(true);
204         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
205         setWillNotDraw(false);
206         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
207         mTouchSlop = configuration.getScaledTouchSlop();
208         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
209         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
210         mOverscrollDistance = configuration.getScaledOverscrollDistance();
211         mOverflingDistance = configuration.getScaledOverflingDistance();
212     }
213 
214     @Override
addView(View child)215     public void addView(View child) {
216         if (getChildCount() > 0) {
217             throw new IllegalStateException("HorizontalScrollView can host only one direct child");
218         }
219 
220         super.addView(child);
221     }
222 
223     @Override
addView(View child, int index)224     public void addView(View child, int index) {
225         if (getChildCount() > 0) {
226             throw new IllegalStateException("HorizontalScrollView can host only one direct child");
227         }
228 
229         super.addView(child, index);
230     }
231 
232     @Override
addView(View child, ViewGroup.LayoutParams params)233     public void addView(View child, ViewGroup.LayoutParams params) {
234         if (getChildCount() > 0) {
235             throw new IllegalStateException("HorizontalScrollView can host only one direct child");
236         }
237 
238         super.addView(child, params);
239     }
240 
241     @Override
addView(View child, int index, ViewGroup.LayoutParams params)242     public void addView(View child, int index, ViewGroup.LayoutParams params) {
243         if (getChildCount() > 0) {
244             throw new IllegalStateException("HorizontalScrollView can host only one direct child");
245         }
246 
247         super.addView(child, index, params);
248     }
249 
250     /**
251      * @return Returns true this HorizontalScrollView can be scrolled
252      */
canScroll()253     private boolean canScroll() {
254         View child = getChildAt(0);
255         if (child != null) {
256             int childWidth = child.getWidth();
257             return getWidth() < childWidth + mPaddingLeft + mPaddingRight ;
258         }
259         return false;
260     }
261 
262     /**
263      * Indicates whether this ScrollView's content is stretched to fill the viewport.
264      *
265      * @return True if the content fills the viewport, false otherwise.
266      */
isFillViewport()267     public boolean isFillViewport() {
268         return mFillViewport;
269     }
270 
271     /**
272      * Indicates this ScrollView whether it should stretch its content width to fill
273      * the viewport or not.
274      *
275      * @param fillViewport True to stretch the content's width to the viewport's
276      *        boundaries, false otherwise.
277      */
setFillViewport(boolean fillViewport)278     public void setFillViewport(boolean fillViewport) {
279         if (fillViewport != mFillViewport) {
280             mFillViewport = fillViewport;
281             requestLayout();
282         }
283     }
284 
285     /**
286      * @return Whether arrow scrolling will animate its transition.
287      */
isSmoothScrollingEnabled()288     public boolean isSmoothScrollingEnabled() {
289         return mSmoothScrollingEnabled;
290     }
291 
292     /**
293      * Set whether arrow scrolling will animate its transition.
294      * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
295      */
setSmoothScrollingEnabled(boolean smoothScrollingEnabled)296     public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
297         mSmoothScrollingEnabled = smoothScrollingEnabled;
298     }
299 
300     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)301     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
302         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
303 
304         if (!mFillViewport) {
305             return;
306         }
307 
308         final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
309         if (widthMode == MeasureSpec.UNSPECIFIED) {
310             return;
311         }
312 
313         if (getChildCount() > 0) {
314             final View child = getChildAt(0);
315             int width = getMeasuredWidth();
316             if (child.getMeasuredWidth() < width) {
317                 final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
318 
319                 int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, mPaddingTop
320                         + mPaddingBottom, lp.height);
321                 width -= mPaddingLeft;
322                 width -= mPaddingRight;
323                 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
324 
325                 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
326             }
327         }
328     }
329 
330     @Override
dispatchKeyEvent(KeyEvent event)331     public boolean dispatchKeyEvent(KeyEvent event) {
332         // Let the focused view and/or our descendants get the key first
333         return super.dispatchKeyEvent(event) || executeKeyEvent(event);
334     }
335 
336     /**
337      * You can call this function yourself to have the scroll view perform
338      * scrolling from a key event, just as if the event had been dispatched to
339      * it by the view hierarchy.
340      *
341      * @param event The key event to execute.
342      * @return Return true if the event was handled, else false.
343      */
executeKeyEvent(KeyEvent event)344     public boolean executeKeyEvent(KeyEvent event) {
345         mTempRect.setEmpty();
346 
347         if (!canScroll()) {
348             if (isFocused()) {
349                 View currentFocused = findFocus();
350                 if (currentFocused == this) currentFocused = null;
351                 View nextFocused = FocusFinder.getInstance().findNextFocus(this,
352                         currentFocused, View.FOCUS_RIGHT);
353                 return nextFocused != null && nextFocused != this &&
354                         nextFocused.requestFocus(View.FOCUS_RIGHT);
355             }
356             return false;
357         }
358 
359         boolean handled = false;
360         if (event.getAction() == KeyEvent.ACTION_DOWN) {
361             switch (event.getKeyCode()) {
362                 case KeyEvent.KEYCODE_DPAD_LEFT:
363                     if (!event.isAltPressed()) {
364                         handled = arrowScroll(View.FOCUS_LEFT);
365                     } else {
366                         handled = fullScroll(View.FOCUS_LEFT);
367                     }
368                     break;
369                 case KeyEvent.KEYCODE_DPAD_RIGHT:
370                     if (!event.isAltPressed()) {
371                         handled = arrowScroll(View.FOCUS_RIGHT);
372                     } else {
373                         handled = fullScroll(View.FOCUS_RIGHT);
374                     }
375                     break;
376                 case KeyEvent.KEYCODE_SPACE:
377                     pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
378                     break;
379             }
380         }
381 
382         return handled;
383     }
384 
inChild(int x, int y)385     private boolean inChild(int x, int y) {
386         if (getChildCount() > 0) {
387             final int scrollX = mScrollX;
388             final View child = getChildAt(0);
389             return !(y < child.getTop()
390                     || y >= child.getBottom()
391                     || x < child.getLeft() - scrollX
392                     || x >= child.getRight() - scrollX);
393         }
394         return false;
395     }
396 
397     @Override
onInterceptTouchEvent(MotionEvent ev)398     public boolean onInterceptTouchEvent(MotionEvent ev) {
399         /*
400          * This method JUST determines whether we want to intercept the motion.
401          * If we return true, onMotionEvent will be called and we do the actual
402          * scrolling there.
403          */
404 
405         /*
406         * Shortcut the most recurring case: the user is in the dragging
407         * state and he is moving his finger.  We want to intercept this
408         * motion.
409         */
410         final int action = ev.getAction();
411         if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
412             return true;
413         }
414 
415         switch (action & MotionEvent.ACTION_MASK) {
416             case MotionEvent.ACTION_MOVE: {
417                 /*
418                  * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
419                  * whether the user has moved far enough from his original down touch.
420                  */
421 
422                 /*
423                 * Locally do absolute value. mLastMotionX is set to the x value
424                 * of the down event.
425                 */
426                 final int activePointerId = mActivePointerId;
427                 if (activePointerId == INVALID_POINTER) {
428                     // If we don't have a valid id, the touch down wasn't on content.
429                     break;
430                 }
431 
432                 final int pointerIndex = ev.findPointerIndex(activePointerId);
433                 final float x = ev.getX(pointerIndex);
434                 final int xDiff = (int) Math.abs(x - mLastMotionX);
435                 if (xDiff > mTouchSlop) {
436                     mIsBeingDragged = true;
437                     mLastMotionX = x;
438                     if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
439                 }
440                 break;
441             }
442 
443             case MotionEvent.ACTION_DOWN: {
444                 final float x = ev.getX();
445                 if (!inChild((int) x, (int) ev.getY())) {
446                     mIsBeingDragged = false;
447                     break;
448                 }
449 
450                 /*
451                  * Remember location of down touch.
452                  * ACTION_DOWN always refers to pointer index 0.
453                  */
454                 mLastMotionX = x;
455                 mActivePointerId = ev.getPointerId(0);
456 
457                 /*
458                 * If being flinged and user touches the screen, initiate drag;
459                 * otherwise don't.  mScroller.isFinished should be false when
460                 * being flinged.
461                 */
462                 mIsBeingDragged = !mScroller.isFinished();
463                 break;
464             }
465 
466             case MotionEvent.ACTION_CANCEL:
467             case MotionEvent.ACTION_UP:
468                 /* Release the drag */
469                 mIsBeingDragged = false;
470                 mActivePointerId = INVALID_POINTER;
471                 if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
472                     invalidate();
473                 }
474                 break;
475             case MotionEvent.ACTION_POINTER_UP:
476                 onSecondaryPointerUp(ev);
477                 break;
478         }
479 
480         /*
481         * The only time we want to intercept motion events is if we are in the
482         * drag mode.
483         */
484         return mIsBeingDragged;
485     }
486 
487     @Override
onTouchEvent(MotionEvent ev)488     public boolean onTouchEvent(MotionEvent ev) {
489 
490         if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
491             // Don't handle edge touches immediately -- they may actually belong to one of our
492             // descendants.
493             return false;
494         }
495 
496         if (mVelocityTracker == null) {
497             mVelocityTracker = VelocityTracker.obtain();
498         }
499         mVelocityTracker.addMovement(ev);
500 
501         final int action = ev.getAction();
502 
503         switch (action & MotionEvent.ACTION_MASK) {
504             case MotionEvent.ACTION_DOWN: {
505                 mIsBeingDragged = getChildCount() != 0;
506                 if (!mIsBeingDragged) {
507                     return false;
508                 }
509 
510                 /*
511                  * If being flinged and user touches, stop the fling. isFinished
512                  * will be false if being flinged.
513                  */
514                 if (!mScroller.isFinished()) {
515                     mScroller.abortAnimation();
516                 }
517 
518                 // Remember where the motion event started
519                 mLastMotionX = ev.getX();
520                 mActivePointerId = ev.getPointerId(0);
521                 break;
522             }
523             case MotionEvent.ACTION_MOVE:
524                 if (mIsBeingDragged) {
525                     // Scroll to follow the motion event
526                     final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
527                     final float x = ev.getX(activePointerIndex);
528                     final int deltaX = (int) (mLastMotionX - x);
529                     mLastMotionX = x;
530 
531                     final int oldX = mScrollX;
532                     final int oldY = mScrollY;
533                     final int range = getScrollRange();
534                     if (overScrollBy(deltaX, 0, mScrollX, 0, range, 0,
535                             mOverscrollDistance, 0, true)) {
536                         // Break our velocity if we hit a scroll barrier.
537                         mVelocityTracker.clear();
538                     }
539                     onScrollChanged(mScrollX, mScrollY, oldX, oldY);
540 
541                     final int overscrollMode = getOverScrollMode();
542                     if (overscrollMode == OVER_SCROLL_ALWAYS ||
543                             (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)) {
544                         final int pulledToX = oldX + deltaX;
545                         if (pulledToX < 0) {
546                             mEdgeGlowLeft.onPull((float) deltaX / getWidth());
547                             if (!mEdgeGlowRight.isFinished()) {
548                                 mEdgeGlowRight.onRelease();
549                             }
550                         } else if (pulledToX > range) {
551                             mEdgeGlowRight.onPull((float) deltaX / getWidth());
552                             if (!mEdgeGlowLeft.isFinished()) {
553                                 mEdgeGlowLeft.onRelease();
554                             }
555                         }
556                         if (mEdgeGlowLeft != null
557                                 && (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) {
558                             invalidate();
559                         }
560                     }
561                 }
562                 break;
563             case MotionEvent.ACTION_UP:
564                 if (mIsBeingDragged) {
565                     final VelocityTracker velocityTracker = mVelocityTracker;
566                     velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
567                     int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
568 
569                     if (getChildCount() > 0) {
570                         if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
571                             fling(-initialVelocity);
572                         } else {
573                             final int right = getScrollRange();
574                             if (mScroller.springBack(mScrollX, mScrollY, 0, right, 0, 0)) {
575                                 invalidate();
576                             }
577                         }
578                     }
579 
580                     mActivePointerId = INVALID_POINTER;
581                     mIsBeingDragged = false;
582 
583                     if (mVelocityTracker != null) {
584                         mVelocityTracker.recycle();
585                         mVelocityTracker = null;
586                     }
587                     if (mEdgeGlowLeft != null) {
588                         mEdgeGlowLeft.onRelease();
589                         mEdgeGlowRight.onRelease();
590                     }
591                 }
592                 break;
593             case MotionEvent.ACTION_CANCEL:
594                 if (mIsBeingDragged && getChildCount() > 0) {
595                     if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
596                         invalidate();
597                     }
598                     mActivePointerId = INVALID_POINTER;
599                     mIsBeingDragged = false;
600                     if (mVelocityTracker != null) {
601                         mVelocityTracker.recycle();
602                         mVelocityTracker = null;
603                     }
604                     if (mEdgeGlowLeft != null) {
605                         mEdgeGlowLeft.onRelease();
606                         mEdgeGlowRight.onRelease();
607                     }
608                 }
609                 break;
610             case MotionEvent.ACTION_POINTER_UP:
611                 onSecondaryPointerUp(ev);
612                 break;
613         }
614         return true;
615     }
616 
onSecondaryPointerUp(MotionEvent ev)617     private void onSecondaryPointerUp(MotionEvent ev) {
618         final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
619                 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
620         final int pointerId = ev.getPointerId(pointerIndex);
621         if (pointerId == mActivePointerId) {
622             // This was our active pointer going up. Choose a new
623             // active pointer and adjust accordingly.
624             // TODO: Make this decision more intelligent.
625             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
626             mLastMotionX = ev.getX(newPointerIndex);
627             mActivePointerId = ev.getPointerId(newPointerIndex);
628             if (mVelocityTracker != null) {
629                 mVelocityTracker.clear();
630             }
631         }
632     }
633 
634     @Override
onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)635     protected void onOverScrolled(int scrollX, int scrollY,
636             boolean clampedX, boolean clampedY) {
637         // Treat animating scrolls differently; see #computeScroll() for why.
638         if (!mScroller.isFinished()) {
639             mScrollX = scrollX;
640             mScrollY = scrollY;
641             if (clampedX) {
642                 mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0);
643             }
644         } else {
645             super.scrollTo(scrollX, scrollY);
646         }
647         awakenScrollBars();
648     }
649 
getScrollRange()650     private int getScrollRange() {
651         int scrollRange = 0;
652         if (getChildCount() > 0) {
653             View child = getChildAt(0);
654             scrollRange = Math.max(0,
655                     child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight));
656         }
657         return scrollRange;
658     }
659 
660     /**
661      * <p>
662      * Finds the next focusable component that fits in this View's bounds
663      * (excluding fading edges) pretending that this View's left is located at
664      * the parameter left.
665      * </p>
666      *
667      * @param leftFocus          look for a candidate is the one at the left of the bounds
668      *                           if leftFocus is true, or at the right of the bounds if leftFocus
669      *                           is false
670      * @param left               the left offset of the bounds in which a focusable must be
671      *                           found (the fading edge is assumed to start at this position)
672      * @param preferredFocusable the View that has highest priority and will be
673      *                           returned if it is within my bounds (null is valid)
674      * @return the next focusable component in the bounds or null if none can be found
675      */
findFocusableViewInMyBounds(final boolean leftFocus, final int left, View preferredFocusable)676     private View findFocusableViewInMyBounds(final boolean leftFocus,
677             final int left, View preferredFocusable) {
678         /*
679          * The fading edge's transparent side should be considered for focus
680          * since it's mostly visible, so we divide the actual fading edge length
681          * by 2.
682          */
683         final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
684         final int leftWithoutFadingEdge = left + fadingEdgeLength;
685         final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
686 
687         if ((preferredFocusable != null)
688                 && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
689                 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
690             return preferredFocusable;
691         }
692 
693         return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
694                 rightWithoutFadingEdge);
695     }
696 
697     /**
698      * <p>
699      * Finds the next focusable component that fits in the specified bounds.
700      * </p>
701      *
702      * @param leftFocus look for a candidate is the one at the left of the bounds
703      *                  if leftFocus is true, or at the right of the bounds if
704      *                  leftFocus is false
705      * @param left      the left offset of the bounds in which a focusable must be
706      *                  found
707      * @param right     the right offset of the bounds in which a focusable must
708      *                  be found
709      * @return the next focusable component in the bounds or null if none can
710      *         be found
711      */
findFocusableViewInBounds(boolean leftFocus, int left, int right)712     private View findFocusableViewInBounds(boolean leftFocus, int left, int right) {
713 
714         List<View> focusables = getFocusables(View.FOCUS_FORWARD);
715         View focusCandidate = null;
716 
717         /*
718          * A fully contained focusable is one where its left is below the bound's
719          * left, and its right is above the bound's right. A partially
720          * contained focusable is one where some part of it is within the
721          * bounds, but it also has some part that is not within bounds.  A fully contained
722          * focusable is preferred to a partially contained focusable.
723          */
724         boolean foundFullyContainedFocusable = false;
725 
726         int count = focusables.size();
727         for (int i = 0; i < count; i++) {
728             View view = focusables.get(i);
729             int viewLeft = view.getLeft();
730             int viewRight = view.getRight();
731 
732             if (left < viewRight && viewLeft < right) {
733                 /*
734                  * the focusable is in the target area, it is a candidate for
735                  * focusing
736                  */
737 
738                 final boolean viewIsFullyContained = (left < viewLeft) &&
739                         (viewRight < right);
740 
741                 if (focusCandidate == null) {
742                     /* No candidate, take this one */
743                     focusCandidate = view;
744                     foundFullyContainedFocusable = viewIsFullyContained;
745                 } else {
746                     final boolean viewIsCloserToBoundary =
747                             (leftFocus && viewLeft < focusCandidate.getLeft()) ||
748                                     (!leftFocus && viewRight > focusCandidate.getRight());
749 
750                     if (foundFullyContainedFocusable) {
751                         if (viewIsFullyContained && viewIsCloserToBoundary) {
752                             /*
753                              * We're dealing with only fully contained views, so
754                              * it has to be closer to the boundary to beat our
755                              * candidate
756                              */
757                             focusCandidate = view;
758                         }
759                     } else {
760                         if (viewIsFullyContained) {
761                             /* Any fully contained view beats a partially contained view */
762                             focusCandidate = view;
763                             foundFullyContainedFocusable = true;
764                         } else if (viewIsCloserToBoundary) {
765                             /*
766                              * Partially contained view beats another partially
767                              * contained view if it's closer
768                              */
769                             focusCandidate = view;
770                         }
771                     }
772                 }
773             }
774         }
775 
776         return focusCandidate;
777     }
778 
779     /**
780      * <p>Handles scrolling in response to a "page up/down" shortcut press. This
781      * method will scroll the view by one page left or right and give the focus
782      * to the leftmost/rightmost component in the new visible area. If no
783      * component is a good candidate for focus, this scrollview reclaims the
784      * focus.</p>
785      *
786      * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
787      *                  to go one page left or {@link android.view.View#FOCUS_RIGHT}
788      *                  to go one page right
789      * @return true if the key event is consumed by this method, false otherwise
790      */
pageScroll(int direction)791     public boolean pageScroll(int direction) {
792         boolean right = direction == View.FOCUS_RIGHT;
793         int width = getWidth();
794 
795         if (right) {
796             mTempRect.left = getScrollX() + width;
797             int count = getChildCount();
798             if (count > 0) {
799                 View view = getChildAt(0);
800                 if (mTempRect.left + width > view.getRight()) {
801                     mTempRect.left = view.getRight() - width;
802                 }
803             }
804         } else {
805             mTempRect.left = getScrollX() - width;
806             if (mTempRect.left < 0) {
807                 mTempRect.left = 0;
808             }
809         }
810         mTempRect.right = mTempRect.left + width;
811 
812         return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
813     }
814 
815     /**
816      * <p>Handles scrolling in response to a "home/end" shortcut press. This
817      * method will scroll the view to the left or right and give the focus
818      * to the leftmost/rightmost component in the new visible area. If no
819      * component is a good candidate for focus, this scrollview reclaims the
820      * focus.</p>
821      *
822      * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
823      *                  to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
824      *                  to go the right
825      * @return true if the key event is consumed by this method, false otherwise
826      */
fullScroll(int direction)827     public boolean fullScroll(int direction) {
828         boolean right = direction == View.FOCUS_RIGHT;
829         int width = getWidth();
830 
831         mTempRect.left = 0;
832         mTempRect.right = width;
833 
834         if (right) {
835             int count = getChildCount();
836             if (count > 0) {
837                 View view = getChildAt(0);
838                 mTempRect.right = view.getRight();
839                 mTempRect.left = mTempRect.right - width;
840             }
841         }
842 
843         return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
844     }
845 
846     /**
847      * <p>Scrolls the view to make the area defined by <code>left</code> and
848      * <code>right</code> visible. This method attempts to give the focus
849      * to a component visible in this area. If no component can be focused in
850      * the new visible area, the focus is reclaimed by this scrollview.</p>
851      *
852      * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
853      *                  to go left {@link android.view.View#FOCUS_RIGHT} to right
854      * @param left     the left offset of the new area to be made visible
855      * @param right    the right offset of the new area to be made visible
856      * @return true if the key event is consumed by this method, false otherwise
857      */
scrollAndFocus(int direction, int left, int right)858     private boolean scrollAndFocus(int direction, int left, int right) {
859         boolean handled = true;
860 
861         int width = getWidth();
862         int containerLeft = getScrollX();
863         int containerRight = containerLeft + width;
864         boolean goLeft = direction == View.FOCUS_LEFT;
865 
866         View newFocused = findFocusableViewInBounds(goLeft, left, right);
867         if (newFocused == null) {
868             newFocused = this;
869         }
870 
871         if (left >= containerLeft && right <= containerRight) {
872             handled = false;
873         } else {
874             int delta = goLeft ? (left - containerLeft) : (right - containerRight);
875             doScrollX(delta);
876         }
877 
878         if (newFocused != findFocus() && newFocused.requestFocus(direction)) {
879             mScrollViewMovedFocus = true;
880             mScrollViewMovedFocus = false;
881         }
882 
883         return handled;
884     }
885 
886     /**
887      * Handle scrolling in response to a left or right arrow click.
888      *
889      * @param direction The direction corresponding to the arrow key that was
890      *                  pressed
891      * @return True if we consumed the event, false otherwise
892      */
arrowScroll(int direction)893     public boolean arrowScroll(int direction) {
894 
895         View currentFocused = findFocus();
896         if (currentFocused == this) currentFocused = null;
897 
898         View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
899 
900         final int maxJump = getMaxScrollAmount();
901 
902         if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
903             nextFocused.getDrawingRect(mTempRect);
904             offsetDescendantRectToMyCoords(nextFocused, mTempRect);
905             int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
906             doScrollX(scrollDelta);
907             nextFocused.requestFocus(direction);
908         } else {
909             // no new focus
910             int scrollDelta = maxJump;
911 
912             if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
913                 scrollDelta = getScrollX();
914             } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) {
915 
916                 int daRight = getChildAt(0).getRight();
917 
918                 int screenRight = getScrollX() + getWidth();
919 
920                 if (daRight - screenRight < maxJump) {
921                     scrollDelta = daRight - screenRight;
922                 }
923             }
924             if (scrollDelta == 0) {
925                 return false;
926             }
927             doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta);
928         }
929 
930         if (currentFocused != null && currentFocused.isFocused()
931                 && isOffScreen(currentFocused)) {
932             // previously focused item still has focus and is off screen, give
933             // it up (take it back to ourselves)
934             // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
935             // sure to
936             // get it)
937             final int descendantFocusability = getDescendantFocusability();  // save
938             setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
939             requestFocus();
940             setDescendantFocusability(descendantFocusability);  // restore
941         }
942         return true;
943     }
944 
945     /**
946      * @return whether the descendant of this scroll view is scrolled off
947      *  screen.
948      */
isOffScreen(View descendant)949     private boolean isOffScreen(View descendant) {
950         return !isWithinDeltaOfScreen(descendant, 0);
951     }
952 
953     /**
954      * @return whether the descendant of this scroll view is within delta
955      *  pixels of being on the screen.
956      */
isWithinDeltaOfScreen(View descendant, int delta)957     private boolean isWithinDeltaOfScreen(View descendant, int delta) {
958         descendant.getDrawingRect(mTempRect);
959         offsetDescendantRectToMyCoords(descendant, mTempRect);
960 
961         return (mTempRect.right + delta) >= getScrollX()
962                 && (mTempRect.left - delta) <= (getScrollX() + getWidth());
963     }
964 
965     /**
966      * Smooth scroll by a X delta
967      *
968      * @param delta the number of pixels to scroll by on the X axis
969      */
doScrollX(int delta)970     private void doScrollX(int delta) {
971         if (delta != 0) {
972             if (mSmoothScrollingEnabled) {
973                 smoothScrollBy(delta, 0);
974             } else {
975                 scrollBy(delta, 0);
976             }
977         }
978     }
979 
980     /**
981      * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
982      *
983      * @param dx the number of pixels to scroll by on the X axis
984      * @param dy the number of pixels to scroll by on the Y axis
985      */
smoothScrollBy(int dx, int dy)986     public final void smoothScrollBy(int dx, int dy) {
987         if (getChildCount() == 0) {
988             // Nothing to do.
989             return;
990         }
991         long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
992         if (duration > ANIMATED_SCROLL_GAP) {
993             final int width = getWidth() - mPaddingRight - mPaddingLeft;
994             final int right = getChildAt(0).getWidth();
995             final int maxX = Math.max(0, right - width);
996             final int scrollX = mScrollX;
997             dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
998 
999             mScroller.startScroll(scrollX, mScrollY, dx, 0);
1000             invalidate();
1001         } else {
1002             if (!mScroller.isFinished()) {
1003                 mScroller.abortAnimation();
1004             }
1005             scrollBy(dx, dy);
1006         }
1007         mLastScroll = AnimationUtils.currentAnimationTimeMillis();
1008     }
1009 
1010     /**
1011      * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
1012      *
1013      * @param x the position where to scroll on the X axis
1014      * @param y the position where to scroll on the Y axis
1015      */
smoothScrollTo(int x, int y)1016     public final void smoothScrollTo(int x, int y) {
1017         smoothScrollBy(x - mScrollX, y - mScrollY);
1018     }
1019 
1020     /**
1021      * <p>The scroll range of a scroll view is the overall width of all of its
1022      * children.</p>
1023      */
1024     @Override
computeHorizontalScrollRange()1025     protected int computeHorizontalScrollRange() {
1026         final int count = getChildCount();
1027         final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight;
1028         if (count == 0) {
1029             return contentWidth;
1030         }
1031 
1032         int scrollRange = getChildAt(0).getRight();
1033         final int scrollX = mScrollX;
1034         final int overscrollRight = Math.max(0, scrollRange - contentWidth);
1035         if (scrollX < 0) {
1036             scrollRange -= scrollX;
1037         } else if (scrollX > overscrollRight) {
1038             scrollRange += scrollX - overscrollRight;
1039         }
1040 
1041         return scrollRange;
1042     }
1043 
1044     @Override
computeHorizontalScrollOffset()1045     protected int computeHorizontalScrollOffset() {
1046         return Math.max(0, super.computeHorizontalScrollOffset());
1047     }
1048 
1049     @Override
measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)1050     protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
1051         ViewGroup.LayoutParams lp = child.getLayoutParams();
1052 
1053         int childWidthMeasureSpec;
1054         int childHeightMeasureSpec;
1055 
1056         childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop
1057                 + mPaddingBottom, lp.height);
1058 
1059         childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1060 
1061         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1062     }
1063 
1064     @Override
measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)1065     protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
1066             int parentHeightMeasureSpec, int heightUsed) {
1067         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
1068 
1069         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
1070                 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
1071                         + heightUsed, lp.height);
1072         final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
1073                 lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
1074 
1075         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1076     }
1077 
1078     @Override
computeScroll()1079     public void computeScroll() {
1080         if (mScroller.computeScrollOffset()) {
1081             // This is called at drawing time by ViewGroup.  We don't want to
1082             // re-show the scrollbars at this point, which scrollTo will do,
1083             // so we replicate most of scrollTo here.
1084             //
1085             //         It's a little odd to call onScrollChanged from inside the drawing.
1086             //
1087             //         It is, except when you remember that computeScroll() is used to
1088             //         animate scrolling. So unless we want to defer the onScrollChanged()
1089             //         until the end of the animated scrolling, we don't really have a
1090             //         choice here.
1091             //
1092             //         I agree.  The alternative, which I think would be worse, is to post
1093             //         something and tell the subclasses later.  This is bad because there
1094             //         will be a window where mScrollX/Y is different from what the app
1095             //         thinks it is.
1096             //
1097             int oldX = mScrollX;
1098             int oldY = mScrollY;
1099             int x = mScroller.getCurrX();
1100             int y = mScroller.getCurrY();
1101 
1102             if (oldX != x || oldY != y) {
1103                 overScrollBy(x - oldX, y - oldY, oldX, oldY, getScrollRange(), 0,
1104                         mOverflingDistance, 0, false);
1105                 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
1106 
1107                 final int range = getScrollRange();
1108                 final int overscrollMode = getOverScrollMode();
1109                 if (overscrollMode == OVER_SCROLL_ALWAYS ||
1110                         (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)) {
1111                     if (x < 0 && oldX >= 0) {
1112                         mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
1113                     } else if (x > range && oldX <= range) {
1114                         mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
1115                     }
1116                 }
1117             }
1118             awakenScrollBars();
1119 
1120             // Keep on drawing until the animation has finished.
1121             postInvalidate();
1122         }
1123     }
1124 
1125     /**
1126      * Scrolls the view to the given child.
1127      *
1128      * @param child the View to scroll to
1129      */
scrollToChild(View child)1130     private void scrollToChild(View child) {
1131         child.getDrawingRect(mTempRect);
1132 
1133         /* Offset from child's local coordinates to ScrollView coordinates */
1134         offsetDescendantRectToMyCoords(child, mTempRect);
1135 
1136         int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1137 
1138         if (scrollDelta != 0) {
1139             scrollBy(scrollDelta, 0);
1140         }
1141     }
1142 
1143     /**
1144      * If rect is off screen, scroll just enough to get it (or at least the
1145      * first screen size chunk of it) on screen.
1146      *
1147      * @param rect      The rectangle.
1148      * @param immediate True to scroll immediately without animation
1149      * @return true if scrolling was performed
1150      */
scrollToChildRect(Rect rect, boolean immediate)1151     private boolean scrollToChildRect(Rect rect, boolean immediate) {
1152         final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
1153         final boolean scroll = delta != 0;
1154         if (scroll) {
1155             if (immediate) {
1156                 scrollBy(delta, 0);
1157             } else {
1158                 smoothScrollBy(delta, 0);
1159             }
1160         }
1161         return scroll;
1162     }
1163 
1164     /**
1165      * Compute the amount to scroll in the X direction in order to get
1166      * a rectangle completely on the screen (or, if taller than the screen,
1167      * at least the first screen size chunk of it).
1168      *
1169      * @param rect The rect.
1170      * @return The scroll delta.
1171      */
computeScrollDeltaToGetChildRectOnScreen(Rect rect)1172     protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
1173         if (getChildCount() == 0) return 0;
1174 
1175         int width = getWidth();
1176         int screenLeft = getScrollX();
1177         int screenRight = screenLeft + width;
1178 
1179         int fadingEdge = getHorizontalFadingEdgeLength();
1180 
1181         // leave room for left fading edge as long as rect isn't at very left
1182         if (rect.left > 0) {
1183             screenLeft += fadingEdge;
1184         }
1185 
1186         // leave room for right fading edge as long as rect isn't at very right
1187         if (rect.right < getChildAt(0).getWidth()) {
1188             screenRight -= fadingEdge;
1189         }
1190 
1191         int scrollXDelta = 0;
1192 
1193         if (rect.right > screenRight && rect.left > screenLeft) {
1194             // need to move right to get it in view: move right just enough so
1195             // that the entire rectangle is in view (or at least the first
1196             // screen size chunk).
1197 
1198             if (rect.width() > width) {
1199                 // just enough to get screen size chunk on
1200                 scrollXDelta += (rect.left - screenLeft);
1201             } else {
1202                 // get entire rect at right of screen
1203                 scrollXDelta += (rect.right - screenRight);
1204             }
1205 
1206             // make sure we aren't scrolling beyond the end of our content
1207             int right = getChildAt(0).getRight();
1208             int distanceToRight = right - screenRight;
1209             scrollXDelta = Math.min(scrollXDelta, distanceToRight);
1210 
1211         } else if (rect.left < screenLeft && rect.right < screenRight) {
1212             // need to move right to get it in view: move right just enough so that
1213             // entire rectangle is in view (or at least the first screen
1214             // size chunk of it).
1215 
1216             if (rect.width() > width) {
1217                 // screen size chunk
1218                 scrollXDelta -= (screenRight - rect.right);
1219             } else {
1220                 // entire rect at left
1221                 scrollXDelta -= (screenLeft - rect.left);
1222             }
1223 
1224             // make sure we aren't scrolling any further than the left our content
1225             scrollXDelta = Math.max(scrollXDelta, -getScrollX());
1226         }
1227         return scrollXDelta;
1228     }
1229 
1230     @Override
requestChildFocus(View child, View focused)1231     public void requestChildFocus(View child, View focused) {
1232         if (!mScrollViewMovedFocus) {
1233             if (!mIsLayoutDirty) {
1234                 scrollToChild(focused);
1235             } else {
1236                 // The child may not be laid out yet, we can't compute the scroll yet
1237                 mChildToScrollTo = focused;
1238             }
1239         }
1240         super.requestChildFocus(child, focused);
1241     }
1242 
1243 
1244     /**
1245      * When looking for focus in children of a scroll view, need to be a little
1246      * more careful not to give focus to something that is scrolled off screen.
1247      *
1248      * This is more expensive than the default {@link android.view.ViewGroup}
1249      * implementation, otherwise this behavior might have been made the default.
1250      */
1251     @Override
onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)1252     protected boolean onRequestFocusInDescendants(int direction,
1253             Rect previouslyFocusedRect) {
1254 
1255         // convert from forward / backward notation to up / down / left / right
1256         // (ugh).
1257         if (direction == View.FOCUS_FORWARD) {
1258             direction = View.FOCUS_RIGHT;
1259         } else if (direction == View.FOCUS_BACKWARD) {
1260             direction = View.FOCUS_LEFT;
1261         }
1262 
1263         final View nextFocus = previouslyFocusedRect == null ?
1264                 FocusFinder.getInstance().findNextFocus(this, null, direction) :
1265                 FocusFinder.getInstance().findNextFocusFromRect(this,
1266                         previouslyFocusedRect, direction);
1267 
1268         if (nextFocus == null) {
1269             return false;
1270         }
1271 
1272         if (isOffScreen(nextFocus)) {
1273             return false;
1274         }
1275 
1276         return nextFocus.requestFocus(direction, previouslyFocusedRect);
1277     }
1278 
1279     @Override
requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)1280     public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
1281             boolean immediate) {
1282         // offset into coordinate space of this scroll view
1283         rectangle.offset(child.getLeft() - child.getScrollX(),
1284                 child.getTop() - child.getScrollY());
1285 
1286         return scrollToChildRect(rectangle, immediate);
1287     }
1288 
1289     @Override
requestLayout()1290     public void requestLayout() {
1291         mIsLayoutDirty = true;
1292         super.requestLayout();
1293     }
1294 
1295     @Override
onLayout(boolean changed, int l, int t, int r, int b)1296     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1297         super.onLayout(changed, l, t, r, b);
1298         mIsLayoutDirty = false;
1299         // Give a child focus if it needs it
1300         if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
1301                 scrollToChild(mChildToScrollTo);
1302         }
1303         mChildToScrollTo = null;
1304 
1305         // Calling this with the present values causes it to re-clam them
1306         scrollTo(mScrollX, mScrollY);
1307     }
1308 
1309     @Override
onSizeChanged(int w, int h, int oldw, int oldh)1310     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1311         super.onSizeChanged(w, h, oldw, oldh);
1312 
1313         View currentFocused = findFocus();
1314         if (null == currentFocused || this == currentFocused)
1315             return;
1316 
1317         final int maxJump = mRight - mLeft;
1318 
1319         if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
1320             currentFocused.getDrawingRect(mTempRect);
1321             offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1322             int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1323             doScrollX(scrollDelta);
1324         }
1325     }
1326 
1327     /**
1328      * Return true if child is an descendant of parent, (or equal to the parent).
1329      */
isViewDescendantOf(View child, View parent)1330     private boolean isViewDescendantOf(View child, View parent) {
1331         if (child == parent) {
1332             return true;
1333         }
1334 
1335         final ViewParent theParent = child.getParent();
1336         return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1337     }
1338 
1339     /**
1340      * Fling the scroll view
1341      *
1342      * @param velocityX The initial velocity in the X direction. Positive
1343      *                  numbers mean that the finger/curor is moving down the screen,
1344      *                  which means we want to scroll towards the left.
1345      */
fling(int velocityX)1346     public void fling(int velocityX) {
1347         if (getChildCount() > 0) {
1348             int width = getWidth() - mPaddingRight - mPaddingLeft;
1349             int right = getChildAt(0).getWidth();
1350 
1351             mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
1352                     Math.max(0, right - width), 0, 0, width/2, 0);
1353 
1354             final boolean movingRight = velocityX > 0;
1355 
1356             View newFocused = findFocusableViewInMyBounds(movingRight,
1357                     mScroller.getFinalX(), findFocus());
1358 
1359             if (newFocused == null) {
1360                 newFocused = this;
1361             }
1362 
1363             if (newFocused != findFocus()
1364                     && newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT)) {
1365                 mScrollViewMovedFocus = true;
1366                 mScrollViewMovedFocus = false;
1367             }
1368 
1369             invalidate();
1370         }
1371     }
1372 
1373     /**
1374      * {@inheritDoc}
1375      *
1376      * <p>This version also clamps the scrolling to the bounds of our child.
1377      */
scrollTo(int x, int y)1378     public void scrollTo(int x, int y) {
1379         // we rely on the fact the View.scrollBy calls scrollTo.
1380         if (getChildCount() > 0) {
1381             View child = getChildAt(0);
1382             x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
1383             y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
1384             if (x != mScrollX || y != mScrollY) {
1385                 super.scrollTo(x, y);
1386             }
1387         }
1388     }
1389 
1390     @Override
setOverScrollMode(int mode)1391     public void setOverScrollMode(int mode) {
1392         if (mode != OVER_SCROLL_NEVER) {
1393             if (mEdgeGlowLeft == null) {
1394                 final Resources res = getContext().getResources();
1395                 final Drawable edge = res.getDrawable(R.drawable.overscroll_edge);
1396                 final Drawable glow = res.getDrawable(R.drawable.overscroll_glow);
1397                 mEdgeGlowLeft = new EdgeGlow(edge, glow);
1398                 mEdgeGlowRight = new EdgeGlow(edge, glow);
1399             }
1400         } else {
1401             mEdgeGlowLeft = null;
1402             mEdgeGlowRight = null;
1403         }
1404         super.setOverScrollMode(mode);
1405     }
1406 
1407     @Override
draw(Canvas canvas)1408     public void draw(Canvas canvas) {
1409         super.draw(canvas);
1410         if (mEdgeGlowLeft != null) {
1411             final int scrollX = mScrollX;
1412             if (!mEdgeGlowLeft.isFinished()) {
1413                 final int restoreCount = canvas.save();
1414                 final int height = getHeight();
1415 
1416                 canvas.rotate(270);
1417                 canvas.translate(-height * 1.5f, Math.min(0, scrollX));
1418                 mEdgeGlowLeft.setSize(getHeight() * 2, getWidth());
1419                 if (mEdgeGlowLeft.draw(canvas)) {
1420                     invalidate();
1421                 }
1422                 canvas.restoreToCount(restoreCount);
1423             }
1424             if (!mEdgeGlowRight.isFinished()) {
1425                 final int restoreCount = canvas.save();
1426                 final int width = getWidth();
1427                 final int height = getHeight();
1428 
1429                 canvas.rotate(90);
1430                 canvas.translate(-height / 2, -(Math.max(getScrollRange(), scrollX) + width));
1431                 mEdgeGlowRight.setSize(height * 2, width);
1432                 if (mEdgeGlowRight.draw(canvas)) {
1433                     invalidate();
1434                 }
1435                 canvas.restoreToCount(restoreCount);
1436             }
1437         }
1438     }
1439 
clamp(int n, int my, int child)1440     private int clamp(int n, int my, int child) {
1441         if (my >= child || n < 0) {
1442             return 0;
1443         }
1444         if ((my + n) > child) {
1445             return child - my;
1446         }
1447         return n;
1448     }
1449 }
1450