blob: f86b37cf8b3adf41f5a62810c0300bc05231153e [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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.util.AttributeSet;
20import android.graphics.Rect;
21import android.view.View;
22import android.view.VelocityTracker;
23import android.view.ViewConfiguration;
24import android.view.ViewGroup;
25import android.view.KeyEvent;
26import android.view.FocusFinder;
27import android.view.MotionEvent;
28import android.view.ViewParent;
29import android.view.animation.AnimationUtils;
30import android.content.Context;
31import android.content.res.TypedArray;
32
33import java.util.List;
34
35/**
36 * Layout container for a view hierarchy that can be scrolled by the user,
37 * allowing it to be larger than the physical display. A HorizontalScrollView
38 * is a {@link FrameLayout}, meaning you should place one child in it
39 * containing the entire contents to scroll; this child may itself be a layout
40 * manager with a complex hierarchy of objects. A child that is often used
41 * is a {@link LinearLayout} in a horizontal orientation, presenting a horizontal
42 * array of top-level items that the user can scroll through.
43 *
44 * <p>You should never use a HorizontalScrollView with a {@link ListView}, since
45 * ListView takes care of its own scrolling. Most importantly, doing this
46 * defeats all of the important optimizations in ListView for dealing with
47 * large lists, since it effectively forces the ListView to display its entire
48 * list of items to fill up the infinite container supplied by HorizontalScrollView.
49 *
50 * <p>The {@link TextView} class also
51 * takes care of its own scrolling, so does not require a ScrollView, but
52 * using the two together is possible to achieve the effect of a text view
53 * within a larger container.
54 *
55 * <p>HorizontalScrollView only supports horizontal scrolling.
56 */
57public class HorizontalScrollView extends FrameLayout {
58 private static final int ANIMATED_SCROLL_GAP = ScrollView.ANIMATED_SCROLL_GAP;
59
60 private static final float MAX_SCROLL_FACTOR = ScrollView.MAX_SCROLL_FACTOR;
61
62
63 private long mLastScroll;
64
65 private final Rect mTempRect = new Rect();
66 private Scroller mScroller;
67
68 /**
69 * Flag to indicate that we are moving focus ourselves. This is so the
70 * code that watches for focus changes initiated outside this ScrollView
71 * knows that it does not have to do anything.
72 */
73 private boolean mScrollViewMovedFocus;
74
75 /**
76 * Position of the last motion event.
77 */
78 private float mLastMotionX;
79
80 /**
81 * True when the layout has changed but the traversal has not come through yet.
82 * Ideally the view hierarchy would keep track of this for us.
83 */
84 private boolean mIsLayoutDirty = true;
85
86 /**
87 * The child to give focus to in the event that a child has requested focus while the
88 * layout is dirty. This prevents the scroll from being wrong if the child has not been
89 * laid out before requesting focus.
90 */
91 private View mChildToScrollTo = null;
92
93 /**
94 * True if the user is currently dragging this ScrollView around. This is
95 * not the same as 'is being flinged', which can be checked by
96 * mScroller.isFinished() (flinging begins when the user lifts his finger).
97 */
98 private boolean mIsBeingDragged = false;
99
100 /**
101 * Determines speed during touch scrolling
102 */
103 private VelocityTracker mVelocityTracker;
104
105 /**
106 * When set to true, the scroll view measure its child to make it fill the currently
107 * visible area.
108 */
109 private boolean mFillViewport;
110
111 /**
112 * Whether arrow scrolling is animated.
113 */
114 private boolean mSmoothScrollingEnabled = true;
115
116 private int mTouchSlop;
Romain Guy4296fc42009-07-06 11:48:52 -0700117 private int mMinimumVelocity;
118 private int mMaximumVelocity;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800119
120 public HorizontalScrollView(Context context) {
121 this(context, null);
122 }
123
124 public HorizontalScrollView(Context context, AttributeSet attrs) {
125 this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle);
126 }
127
128 public HorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
129 super(context, attrs, defStyle);
130 initScrollView();
131
132 TypedArray a = context.obtainStyledAttributes(attrs,
133 android.R.styleable.HorizontalScrollView, defStyle, 0);
134
135 setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false));
136
137 a.recycle();
138 }
139
140 @Override
141 protected float getLeftFadingEdgeStrength() {
142 if (getChildCount() == 0) {
143 return 0.0f;
144 }
145
146 final int length = getHorizontalFadingEdgeLength();
147 if (mScrollX < length) {
148 return mScrollX / (float) length;
149 }
150
151 return 1.0f;
152 }
153
154 @Override
155 protected float getRightFadingEdgeStrength() {
156 if (getChildCount() == 0) {
157 return 0.0f;
158 }
159
160 final int length = getHorizontalFadingEdgeLength();
161 final int rightEdge = getWidth() - mPaddingRight;
162 final int span = getChildAt(0).getRight() - mScrollX - rightEdge;
163 if (span < length) {
164 return span / (float) length;
165 }
166
167 return 1.0f;
168 }
169
170 /**
171 * @return The maximum amount this scroll view will scroll in response to
172 * an arrow event.
173 */
174 public int getMaxScrollAmount() {
175 return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft));
176 }
177
178
179 private void initScrollView() {
180 mScroller = new Scroller(getContext());
181 setFocusable(true);
182 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
183 setWillNotDraw(false);
Romain Guy4296fc42009-07-06 11:48:52 -0700184 final ViewConfiguration configuration = ViewConfiguration.get(mContext);
185 mTouchSlop = configuration.getScaledTouchSlop();
186 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
187 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800188 }
189
190 @Override
191 public void addView(View child) {
192 if (getChildCount() > 0) {
193 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
194 }
195
196 super.addView(child);
197 }
198
199 @Override
200 public void addView(View child, int index) {
201 if (getChildCount() > 0) {
202 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
203 }
204
205 super.addView(child, index);
206 }
207
208 @Override
209 public void addView(View child, ViewGroup.LayoutParams params) {
210 if (getChildCount() > 0) {
211 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
212 }
213
214 super.addView(child, params);
215 }
216
217 @Override
218 public void addView(View child, int index, ViewGroup.LayoutParams params) {
219 if (getChildCount() > 0) {
220 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
221 }
222
223 super.addView(child, index, params);
224 }
225
226 /**
227 * @return Returns true this HorizontalScrollView can be scrolled
228 */
229 private boolean canScroll() {
230 View child = getChildAt(0);
231 if (child != null) {
232 int childWidth = child.getWidth();
233 return getWidth() < childWidth + mPaddingLeft + mPaddingRight ;
234 }
235 return false;
236 }
237
238 /**
239 * Indicates whether this ScrollView's content is stretched to fill the viewport.
240 *
241 * @return True if the content fills the viewport, false otherwise.
242 */
243 public boolean isFillViewport() {
244 return mFillViewport;
245 }
246
247 /**
248 * Indicates this ScrollView whether it should stretch its content width to fill
249 * the viewport or not.
250 *
251 * @param fillViewport True to stretch the content's width to the viewport's
252 * boundaries, false otherwise.
253 */
254 public void setFillViewport(boolean fillViewport) {
255 if (fillViewport != mFillViewport) {
256 mFillViewport = fillViewport;
257 requestLayout();
258 }
259 }
260
261 /**
262 * @return Whether arrow scrolling will animate its transition.
263 */
264 public boolean isSmoothScrollingEnabled() {
265 return mSmoothScrollingEnabled;
266 }
267
268 /**
269 * Set whether arrow scrolling will animate its transition.
270 * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
271 */
272 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
273 mSmoothScrollingEnabled = smoothScrollingEnabled;
274 }
275
276 @Override
277 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
278 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
279
280 if (!mFillViewport) {
281 return;
282 }
283
284 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
285 if (widthMode == MeasureSpec.UNSPECIFIED) {
286 return;
287 }
288
289 final View child = getChildAt(0);
290 int width = getMeasuredWidth();
Romain Guyf83f59f2009-05-04 14:15:23 -0700291 if (child.getMeasuredWidth() < width) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800292 final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
293
294 int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, mPaddingTop
295 + mPaddingBottom, lp.height);
296 width -= mPaddingLeft;
297 width -= mPaddingRight;
298 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
299
300 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
301 }
302 }
303
304 @Override
305 public boolean dispatchKeyEvent(KeyEvent event) {
306 // Let the focused view and/or our descendants get the key first
307 boolean handled = super.dispatchKeyEvent(event);
308 if (handled) {
309 return true;
310 }
311 return executeKeyEvent(event);
312 }
313
314 /**
315 * You can call this function yourself to have the scroll view perform
316 * scrolling from a key event, just as if the event had been dispatched to
317 * it by the view hierarchy.
318 *
319 * @param event The key event to execute.
320 * @return Return true if the event was handled, else false.
321 */
322 public boolean executeKeyEvent(KeyEvent event) {
323 mTempRect.setEmpty();
324
325 if (!canScroll()) {
326 if (isFocused()) {
327 View currentFocused = findFocus();
328 if (currentFocused == this) currentFocused = null;
329 View nextFocused = FocusFinder.getInstance().findNextFocus(this,
330 currentFocused, View.FOCUS_RIGHT);
331 return nextFocused != null && nextFocused != this &&
332 nextFocused.requestFocus(View.FOCUS_RIGHT);
333 }
334 return false;
335 }
336
337 boolean handled = false;
338 if (event.getAction() == KeyEvent.ACTION_DOWN) {
339 switch (event.getKeyCode()) {
340 case KeyEvent.KEYCODE_DPAD_LEFT:
341 if (!event.isAltPressed()) {
342 handled = arrowScroll(View.FOCUS_LEFT);
343 } else {
344 handled = fullScroll(View.FOCUS_LEFT);
345 }
346 break;
347 case KeyEvent.KEYCODE_DPAD_RIGHT:
348 if (!event.isAltPressed()) {
349 handled = arrowScroll(View.FOCUS_RIGHT);
350 } else {
351 handled = fullScroll(View.FOCUS_RIGHT);
352 }
353 break;
354 case KeyEvent.KEYCODE_SPACE:
355 pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
356 break;
357 }
358 }
359
360 return handled;
361 }
362
363 @Override
364 public boolean onInterceptTouchEvent(MotionEvent ev) {
365 /*
366 * This method JUST determines whether we want to intercept the motion.
367 * If we return true, onMotionEvent will be called and we do the actual
368 * scrolling there.
369 */
370
371 /*
372 * Shortcut the most recurring case: the user is in the dragging
373 * state and he is moving his finger. We want to intercept this
374 * motion.
375 */
376 final int action = ev.getAction();
377 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
378 return true;
379 }
380
381 if (!canScroll()) {
382 mIsBeingDragged = false;
383 return false;
384 }
385
386 final float x = ev.getX();
387
388 switch (action) {
389 case MotionEvent.ACTION_MOVE:
390 /*
391 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
392 * whether the user has moved far enough from his original down touch.
393 */
394
395 /*
396 * Locally do absolute value. mLastMotionX is set to the x value
397 * of the down event.
398 */
399 final int xDiff = (int) Math.abs(x - mLastMotionX);
400 if (xDiff > mTouchSlop) {
401 mIsBeingDragged = true;
402 if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
403 }
404 break;
405
406 case MotionEvent.ACTION_DOWN:
407 /* Remember location of down touch */
408 mLastMotionX = x;
409
410 /*
411 * If being flinged and user touches the screen, initiate drag;
412 * otherwise don't. mScroller.isFinished should be false when
413 * being flinged.
414 */
415 mIsBeingDragged = !mScroller.isFinished();
416 break;
417
418 case MotionEvent.ACTION_CANCEL:
419 case MotionEvent.ACTION_UP:
420 /* Release the drag */
421 mIsBeingDragged = false;
422 break;
423 }
424
425 /*
426 * The only time we want to intercept motion events is if we are in the
427 * drag mode.
428 */
429 return mIsBeingDragged;
430 }
431
432 @Override
433 public boolean onTouchEvent(MotionEvent ev) {
434
435 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
436 // Don't handle edge touches immediately -- they may actually belong to one of our
437 // descendants.
438 return false;
439 }
440
441 if (!canScroll()) {
442 return false;
443 }
444
445 if (mVelocityTracker == null) {
446 mVelocityTracker = VelocityTracker.obtain();
447 }
448 mVelocityTracker.addMovement(ev);
449
450 final int action = ev.getAction();
451 final float x = ev.getX();
452
453 switch (action) {
454 case MotionEvent.ACTION_DOWN:
455 /*
456 * If being flinged and user touches, stop the fling. isFinished
457 * will be false if being flinged.
458 */
459 if (!mScroller.isFinished()) {
460 mScroller.abortAnimation();
461 }
462
463 // Remember where the motion event started
464 mLastMotionX = x;
465 break;
466 case MotionEvent.ACTION_MOVE:
467 // Scroll to follow the motion event
468 final int deltaX = (int) (mLastMotionX - x);
469 mLastMotionX = x;
470
471 if (deltaX < 0) {
472 if (mScrollX > 0) {
473 scrollBy(deltaX, 0);
474 }
475 } else if (deltaX > 0) {
476 final int rightEdge = getWidth() - mPaddingRight;
477 final int availableToScroll = getChildAt(0).getRight() - mScrollX - rightEdge;
478 if (availableToScroll > 0) {
479 scrollBy(Math.min(availableToScroll, deltaX), 0);
480 }
481 }
482 break;
483 case MotionEvent.ACTION_UP:
484 final VelocityTracker velocityTracker = mVelocityTracker;
Romain Guy4296fc42009-07-06 11:48:52 -0700485 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800486 int initialVelocity = (int) velocityTracker.getXVelocity();
487
Romain Guy4296fc42009-07-06 11:48:52 -0700488 if ((Math.abs(initialVelocity) > mMinimumVelocity) && getChildCount() > 0) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800489 fling(-initialVelocity);
490 }
491
492 if (mVelocityTracker != null) {
493 mVelocityTracker.recycle();
494 mVelocityTracker = null;
495 }
496 }
497 return true;
498 }
499
500 /**
501 * <p>
502 * Finds the next focusable component that fits in this View's bounds
503 * (excluding fading edges) pretending that this View's left is located at
504 * the parameter left.
505 * </p>
506 *
507 * @param leftFocus look for a candidate is the one at the left of the bounds
508 * if leftFocus is true, or at the right of the bounds if leftFocus
509 * is false
510 * @param left the left offset of the bounds in which a focusable must be
511 * found (the fading edge is assumed to start at this position)
512 * @param preferredFocusable the View that has highest priority and will be
513 * returned if it is within my bounds (null is valid)
514 * @return the next focusable component in the bounds or null if none can be found
515 */
516 private View findFocusableViewInMyBounds(final boolean leftFocus,
517 final int left, View preferredFocusable) {
518 /*
519 * The fading edge's transparent side should be considered for focus
520 * since it's mostly visible, so we divide the actual fading edge length
521 * by 2.
522 */
523 final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
524 final int leftWithoutFadingEdge = left + fadingEdgeLength;
525 final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
526
527 if ((preferredFocusable != null)
528 && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
529 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
530 return preferredFocusable;
531 }
532
533 return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
534 rightWithoutFadingEdge);
535 }
536
537 /**
538 * <p>
539 * Finds the next focusable component that fits in the specified bounds.
540 * </p>
541 *
542 * @param leftFocus look for a candidate is the one at the left of the bounds
543 * if leftFocus is true, or at the right of the bounds if
544 * leftFocus is false
545 * @param left the left offset of the bounds in which a focusable must be
546 * found
547 * @param right the right offset of the bounds in which a focusable must
548 * be found
549 * @return the next focusable component in the bounds or null if none can
550 * be found
551 */
552 private View findFocusableViewInBounds(boolean leftFocus, int left, int right) {
553
554 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
555 View focusCandidate = null;
556
557 /*
558 * A fully contained focusable is one where its left is below the bound's
559 * left, and its right is above the bound's right. A partially
560 * contained focusable is one where some part of it is within the
561 * bounds, but it also has some part that is not within bounds. A fully contained
562 * focusable is preferred to a partially contained focusable.
563 */
564 boolean foundFullyContainedFocusable = false;
565
566 int count = focusables.size();
567 for (int i = 0; i < count; i++) {
568 View view = focusables.get(i);
569 int viewLeft = view.getLeft();
570 int viewRight = view.getRight();
571
572 if (left < viewRight && viewLeft < right) {
573 /*
574 * the focusable is in the target area, it is a candidate for
575 * focusing
576 */
577
578 final boolean viewIsFullyContained = (left < viewLeft) &&
579 (viewRight < right);
580
581 if (focusCandidate == null) {
582 /* No candidate, take this one */
583 focusCandidate = view;
584 foundFullyContainedFocusable = viewIsFullyContained;
585 } else {
586 final boolean viewIsCloserToBoundary =
587 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
588 (!leftFocus && viewRight > focusCandidate.getRight());
589
590 if (foundFullyContainedFocusable) {
591 if (viewIsFullyContained && viewIsCloserToBoundary) {
592 /*
593 * We're dealing with only fully contained views, so
594 * it has to be closer to the boundary to beat our
595 * candidate
596 */
597 focusCandidate = view;
598 }
599 } else {
600 if (viewIsFullyContained) {
601 /* Any fully contained view beats a partially contained view */
602 focusCandidate = view;
603 foundFullyContainedFocusable = true;
604 } else if (viewIsCloserToBoundary) {
605 /*
606 * Partially contained view beats another partially
607 * contained view if it's closer
608 */
609 focusCandidate = view;
610 }
611 }
612 }
613 }
614 }
615
616 return focusCandidate;
617 }
618
619 /**
620 * <p>Handles scrolling in response to a "page up/down" shortcut press. This
621 * method will scroll the view by one page left or right and give the focus
622 * to the leftmost/rightmost component in the new visible area. If no
623 * component is a good candidate for focus, this scrollview reclaims the
624 * focus.</p>
625 *
626 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
627 * to go one page left or {@link android.view.View#FOCUS_RIGHT}
628 * to go one page right
629 * @return true if the key event is consumed by this method, false otherwise
630 */
631 public boolean pageScroll(int direction) {
632 boolean right = direction == View.FOCUS_RIGHT;
633 int width = getWidth();
634
635 if (right) {
636 mTempRect.left = getScrollX() + width;
637 int count = getChildCount();
638 if (count > 0) {
639 View view = getChildAt(count - 1);
640 if (mTempRect.left + width > view.getRight()) {
641 mTempRect.left = view.getRight() - width;
642 }
643 }
644 } else {
645 mTempRect.left = getScrollX() - width;
646 if (mTempRect.left < 0) {
647 mTempRect.left = 0;
648 }
649 }
650 mTempRect.right = mTempRect.left + width;
651
652 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
653 }
654
655 /**
656 * <p>Handles scrolling in response to a "home/end" shortcut press. This
657 * method will scroll the view to the left or right and give the focus
658 * to the leftmost/rightmost component in the new visible area. If no
659 * component is a good candidate for focus, this scrollview reclaims the
660 * focus.</p>
661 *
662 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
663 * to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
664 * to go the right
665 * @return true if the key event is consumed by this method, false otherwise
666 */
667 public boolean fullScroll(int direction) {
668 boolean right = direction == View.FOCUS_RIGHT;
669 int width = getWidth();
670
671 mTempRect.left = 0;
672 mTempRect.right = width;
673
674 if (right) {
675 int count = getChildCount();
676 if (count > 0) {
677 View view = getChildAt(count - 1);
678 mTempRect.right = view.getRight();
679 mTempRect.left = mTempRect.right - width;
680 }
681 }
682
683 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
684 }
685
686 /**
687 * <p>Scrolls the view to make the area defined by <code>left</code> and
688 * <code>right</code> visible. This method attempts to give the focus
689 * to a component visible in this area. If no component can be focused in
690 * the new visible area, the focus is reclaimed by this scrollview.</p>
691 *
692 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
693 * to go left {@link android.view.View#FOCUS_RIGHT} to right
694 * @param left the left offset of the new area to be made visible
695 * @param right the right offset of the new area to be made visible
696 * @return true if the key event is consumed by this method, false otherwise
697 */
698 private boolean scrollAndFocus(int direction, int left, int right) {
699 boolean handled = true;
700
701 int width = getWidth();
702 int containerLeft = getScrollX();
703 int containerRight = containerLeft + width;
704 boolean goLeft = direction == View.FOCUS_LEFT;
705
706 View newFocused = findFocusableViewInBounds(goLeft, left, right);
707 if (newFocused == null) {
708 newFocused = this;
709 }
710
711 if (left >= containerLeft && right <= containerRight) {
712 handled = false;
713 } else {
714 int delta = goLeft ? (left - containerLeft) : (right - containerRight);
715 doScrollX(delta);
716 }
717
718 if (newFocused != findFocus() && newFocused.requestFocus(direction)) {
719 mScrollViewMovedFocus = true;
720 mScrollViewMovedFocus = false;
721 }
722
723 return handled;
724 }
725
726 /**
727 * Handle scrolling in response to a left or right arrow click.
728 *
729 * @param direction The direction corresponding to the arrow key that was
730 * pressed
731 * @return True if we consumed the event, false otherwise
732 */
733 public boolean arrowScroll(int direction) {
734
735 View currentFocused = findFocus();
736 if (currentFocused == this) currentFocused = null;
737
738 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
739
740 final int maxJump = getMaxScrollAmount();
741
742 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
743 nextFocused.getDrawingRect(mTempRect);
744 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
745 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
746 doScrollX(scrollDelta);
747 nextFocused.requestFocus(direction);
748 } else {
749 // no new focus
750 int scrollDelta = maxJump;
751
752 if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
753 scrollDelta = getScrollX();
754 } else if (direction == View.FOCUS_RIGHT) {
755
756 int daRight = getChildAt(getChildCount() - 1).getRight();
757
758 int screenRight = getScrollX() + getWidth();
759
760 if (daRight - screenRight < maxJump) {
761 scrollDelta = daRight - screenRight;
762 }
763 }
764 if (scrollDelta == 0) {
765 return false;
766 }
767 doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta);
768 }
769
770 if (currentFocused != null && currentFocused.isFocused()
771 && isOffScreen(currentFocused)) {
772 // previously focused item still has focus and is off screen, give
773 // it up (take it back to ourselves)
774 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
775 // sure to
776 // get it)
777 final int descendantFocusability = getDescendantFocusability(); // save
778 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
779 requestFocus();
780 setDescendantFocusability(descendantFocusability); // restore
781 }
782 return true;
783 }
784
785 /**
786 * @return whether the descendant of this scroll view is scrolled off
787 * screen.
788 */
789 private boolean isOffScreen(View descendant) {
790 return !isWithinDeltaOfScreen(descendant, 0);
791 }
792
793 /**
794 * @return whether the descendant of this scroll view is within delta
795 * pixels of being on the screen.
796 */
797 private boolean isWithinDeltaOfScreen(View descendant, int delta) {
798 descendant.getDrawingRect(mTempRect);
799 offsetDescendantRectToMyCoords(descendant, mTempRect);
800
801 return (mTempRect.right + delta) >= getScrollX()
802 && (mTempRect.left - delta) <= (getScrollX() + getWidth());
803 }
804
805 /**
806 * Smooth scroll by a X delta
807 *
808 * @param delta the number of pixels to scroll by on the X axis
809 */
810 private void doScrollX(int delta) {
811 if (delta != 0) {
812 if (mSmoothScrollingEnabled) {
813 smoothScrollBy(delta, 0);
814 } else {
815 scrollBy(delta, 0);
816 }
817 }
818 }
819
820 /**
821 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
822 *
823 * @param dx the number of pixels to scroll by on the X axis
824 * @param dy the number of pixels to scroll by on the Y axis
825 */
826 public final void smoothScrollBy(int dx, int dy) {
827 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
828 if (duration > ANIMATED_SCROLL_GAP) {
829 mScroller.startScroll(mScrollX, mScrollY, dx, dy);
830 invalidate();
831 } else {
832 if (!mScroller.isFinished()) {
833 mScroller.abortAnimation();
834 }
835 scrollBy(dx, dy);
836 }
837 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
838 }
839
840 /**
841 * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
842 *
843 * @param x the position where to scroll on the X axis
844 * @param y the position where to scroll on the Y axis
845 */
846 public final void smoothScrollTo(int x, int y) {
847 smoothScrollBy(x - mScrollX, y - mScrollY);
848 }
849
850 /**
851 * <p>The scroll range of a scroll view is the overall width of all of its
852 * children.</p>
853 */
854 @Override
855 protected int computeHorizontalScrollRange() {
856 int count = getChildCount();
857 return count == 0 ? getWidth() : getChildAt(0).getRight();
858 }
859
860
861 @Override
862 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
863 ViewGroup.LayoutParams lp = child.getLayoutParams();
864
865 int childWidthMeasureSpec;
866 int childHeightMeasureSpec;
867
868 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop
869 + mPaddingBottom, lp.height);
870
871 childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
872
873 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
874 }
875
876 @Override
877 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
878 int parentHeightMeasureSpec, int heightUsed) {
879 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
880
881 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
882 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
883 + heightUsed, lp.height);
884 final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
885 lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
886
887 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
888 }
889
890 @Override
891 public void computeScroll() {
892 if (mScroller.computeScrollOffset()) {
893 // This is called at drawing time by ViewGroup. We don't want to
894 // re-show the scrollbars at this point, which scrollTo will do,
895 // so we replicate most of scrollTo here.
896 //
897 // It's a little odd to call onScrollChanged from inside the drawing.
898 //
899 // It is, except when you remember that computeScroll() is used to
900 // animate scrolling. So unless we want to defer the onScrollChanged()
901 // until the end of the animated scrolling, we don't really have a
902 // choice here.
903 //
904 // I agree. The alternative, which I think would be worse, is to post
905 // something and tell the subclasses later. This is bad because there
906 // will be a window where mScrollX/Y is different from what the app
907 // thinks it is.
908 //
909 int oldX = mScrollX;
910 int oldY = mScrollY;
911 int x = mScroller.getCurrX();
912 int y = mScroller.getCurrY();
913 if (getChildCount() > 0) {
914 View child = getChildAt(0);
915 mScrollX = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
916 mScrollY = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
917 } else {
918 mScrollX = x;
919 mScrollY = y;
920 }
921 if (oldX != mScrollX || oldY != mScrollY) {
922 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
923 }
924
925 // Keep on drawing until the animation has finished.
926 postInvalidate();
927 }
928 }
929
930 /**
931 * Scrolls the view to the given child.
932 *
933 * @param child the View to scroll to
934 */
935 private void scrollToChild(View child) {
936 child.getDrawingRect(mTempRect);
937
938 /* Offset from child's local coordinates to ScrollView coordinates */
939 offsetDescendantRectToMyCoords(child, mTempRect);
940
941 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
942
943 if (scrollDelta != 0) {
944 scrollBy(scrollDelta, 0);
945 }
946 }
947
948 /**
949 * If rect is off screen, scroll just enough to get it (or at least the
950 * first screen size chunk of it) on screen.
951 *
952 * @param rect The rectangle.
953 * @param immediate True to scroll immediately without animation
954 * @return true if scrolling was performed
955 */
956 private boolean scrollToChildRect(Rect rect, boolean immediate) {
957 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
958 final boolean scroll = delta != 0;
959 if (scroll) {
960 if (immediate) {
961 scrollBy(delta, 0);
962 } else {
963 smoothScrollBy(delta, 0);
964 }
965 }
966 return scroll;
967 }
968
969 /**
970 * Compute the amount to scroll in the X direction in order to get
971 * a rectangle completely on the screen (or, if taller than the screen,
972 * at least the first screen size chunk of it).
973 *
974 * @param rect The rect.
975 * @return The scroll delta.
976 */
977 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
978
979 int width = getWidth();
980 int screenLeft = getScrollX();
981 int screenRight = screenLeft + width;
982
983 int fadingEdge = getHorizontalFadingEdgeLength();
984
985 // leave room for left fading edge as long as rect isn't at very left
986 if (rect.left > 0) {
987 screenLeft += fadingEdge;
988 }
989
990 // leave room for right fading edge as long as rect isn't at very right
991 if (rect.right < getChildAt(0).getWidth()) {
992 screenRight -= fadingEdge;
993 }
994
995 int scrollXDelta = 0;
996
997 if (rect.right > screenRight && rect.left > screenLeft) {
998 // need to move right to get it in view: move right just enough so
999 // that the entire rectangle is in view (or at least the first
1000 // screen size chunk).
1001
1002 if (rect.width() > width) {
1003 // just enough to get screen size chunk on
1004 scrollXDelta += (rect.left - screenLeft);
1005 } else {
1006 // get entire rect at right of screen
1007 scrollXDelta += (rect.right - screenRight);
1008 }
1009
1010 // make sure we aren't scrolling beyond the end of our content
1011 int right = getChildAt(getChildCount() - 1).getRight();
1012 int distanceToRight = right - screenRight;
1013 scrollXDelta = Math.min(scrollXDelta, distanceToRight);
1014
1015 } else if (rect.left < screenLeft && rect.right < screenRight) {
1016 // need to move right to get it in view: move right just enough so that
1017 // entire rectangle is in view (or at least the first screen
1018 // size chunk of it).
1019
1020 if (rect.width() > width) {
1021 // screen size chunk
1022 scrollXDelta -= (screenRight - rect.right);
1023 } else {
1024 // entire rect at left
1025 scrollXDelta -= (screenLeft - rect.left);
1026 }
1027
1028 // make sure we aren't scrolling any further than the left our content
1029 scrollXDelta = Math.max(scrollXDelta, -getScrollX());
1030 }
1031 return scrollXDelta;
1032 }
1033
1034 @Override
1035 public void requestChildFocus(View child, View focused) {
1036 if (!mScrollViewMovedFocus) {
1037 if (!mIsLayoutDirty) {
1038 scrollToChild(focused);
1039 } else {
1040 // The child may not be laid out yet, we can't compute the scroll yet
1041 mChildToScrollTo = focused;
1042 }
1043 }
1044 super.requestChildFocus(child, focused);
1045 }
1046
1047
1048 /**
1049 * When looking for focus in children of a scroll view, need to be a little
1050 * more careful not to give focus to something that is scrolled off screen.
1051 *
1052 * This is more expensive than the default {@link android.view.ViewGroup}
1053 * implementation, otherwise this behavior might have been made the default.
1054 */
1055 @Override
1056 protected boolean onRequestFocusInDescendants(int direction,
1057 Rect previouslyFocusedRect) {
1058
1059 // convert from forward / backward notation to up / down / left / right
1060 // (ugh).
1061 if (direction == View.FOCUS_FORWARD) {
1062 direction = View.FOCUS_RIGHT;
1063 } else if (direction == View.FOCUS_BACKWARD) {
1064 direction = View.FOCUS_LEFT;
1065 }
1066
1067 final View nextFocus = previouslyFocusedRect == null ?
1068 FocusFinder.getInstance().findNextFocus(this, null, direction) :
1069 FocusFinder.getInstance().findNextFocusFromRect(this,
1070 previouslyFocusedRect, direction);
1071
1072 if (nextFocus == null) {
1073 return false;
1074 }
1075
1076 if (isOffScreen(nextFocus)) {
1077 return false;
1078 }
1079
1080 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1081 }
1082
1083 @Override
1084 public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
1085 boolean immediate) {
1086 // offset into coordinate space of this scroll view
1087 rectangle.offset(child.getLeft() - child.getScrollX(),
1088 child.getTop() - child.getScrollY());
1089
1090 return scrollToChildRect(rectangle, immediate);
1091 }
1092
1093 @Override
1094 public void requestLayout() {
1095 mIsLayoutDirty = true;
1096 super.requestLayout();
1097 }
1098
1099 @Override
1100 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1101 super.onLayout(changed, l, t, r, b);
1102 mIsLayoutDirty = false;
1103 // Give a child focus if it needs it
1104 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
1105 scrollToChild(mChildToScrollTo);
1106 }
1107 mChildToScrollTo = null;
1108
1109 // Calling this with the present values causes it to re-clam them
1110 scrollTo(mScrollX, mScrollY);
1111 }
1112
1113 @Override
1114 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1115 super.onSizeChanged(w, h, oldw, oldh);
1116
1117 View currentFocused = findFocus();
1118 if (null == currentFocused || this == currentFocused)
1119 return;
1120
1121 final int maxJump = mRight - mLeft;
1122
1123 if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
1124 currentFocused.getDrawingRect(mTempRect);
1125 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1126 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1127 doScrollX(scrollDelta);
1128 }
1129 }
1130
1131 /**
1132 * Return true if child is an descendant of parent, (or equal to the parent).
1133 */
1134 private boolean isViewDescendantOf(View child, View parent) {
1135 if (child == parent) {
1136 return true;
1137 }
1138
1139 final ViewParent theParent = child.getParent();
1140 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1141 }
1142
1143 /**
1144 * Fling the scroll view
1145 *
1146 * @param velocityX The initial velocity in the X direction. Positive
1147 * numbers mean that the finger/curor is moving down the screen,
1148 * which means we want to scroll towards the left.
1149 */
1150 public void fling(int velocityX) {
1151 int width = getWidth() - mPaddingRight - mPaddingLeft;
1152 int right = getChildAt(0).getWidth();
1153
1154 mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0, right - width, 0, 0);
1155
1156 final boolean movingRight = velocityX > 0;
1157
1158 View newFocused = findFocusableViewInMyBounds(movingRight,
1159 mScroller.getFinalX(), findFocus());
1160
1161 if (newFocused == null) {
1162 newFocused = this;
1163 }
1164
1165 if (newFocused != findFocus()
1166 && newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT)) {
1167 mScrollViewMovedFocus = true;
1168 mScrollViewMovedFocus = false;
1169 }
1170
1171 invalidate();
1172 }
1173
1174 /**
1175 * {@inheritDoc}
1176 *
1177 * <p>This version also clamps the scrolling to the bounds of our child.
1178 */
1179 public void scrollTo(int x, int y) {
1180 // we rely on the fact the View.scrollBy calls scrollTo.
1181 if (getChildCount() > 0) {
1182 View child = getChildAt(0);
1183 x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
1184 y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
1185 if (x != mScrollX || y != mScrollY) {
1186 super.scrollTo(x, y);
1187 }
1188 }
1189 }
1190
1191 private int clamp(int n, int my, int child) {
1192 if (my >= child || n < 0) {
1193 return 0;
1194 }
1195 if ((my + n) > child) {
1196 return child - my;
1197 }
1198 return n;
1199 }
1200}