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