1 package com.example.furt.myapplication;
3 import android.content.Context;
4 import android.graphics.Rect;
5 import android.util.AttributeSet;
6 import android.view.FocusFinder;
7 import android.view.KeyEvent;
8 import android.view.MotionEvent;
9 import android.view.VelocityTracker;
10 import android.view.View;
11 import android.view.ViewConfiguration;
12 import android.view.ViewGroup;
13 import android.view.ViewParent;
14 import android.view.animation.AnimationUtils;
15 import android.widget.FrameLayout;
16 import android.widget.Scroller;
18 import java.util.List;
22 * Copyright (C) 2006 The Android Open Source Project
24 * Licensed under the Apache License, Version 2.0 (the "License");
25 * you may not use this file except in compliance with the License.
26 * You may obtain a copy of the License at
28 * http://www.apache.org/licenses/LICENSE-2.0
30 * Unless required by applicable law or agreed to in writing, software
31 * distributed under the License is distributed on an "AS IS" BASIS,
32 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33 * See the License for the specific language governing permissions and
34 * limitations under the License.
37 * Revised 5/19/2010 by GORGES
38 * Now supports two-dimensional view scrolling
43 * Layout container for a view hierarchy that can be scrolled by the user,
44 * allowing it to be larger than the physical display. A TwoDScrollView
45 * is a {@link android.widget.FrameLayout}, meaning you should place one child in it
46 * containing the entire contents to scroll; this child may itself be a layout
47 * manager with a complex hierarchy of objects. A child that is often used
48 * is a {@link android.widget.LinearLayout} in a vertical orientation, presenting a vertical
49 * array of top-level items that the user can scroll through.
51 * <p>The {@link android.widget.TextView} class also
52 * takes care of its own scrolling, so does not require a TwoDScrollView, but
53 * using the two together is possible to achieve the effect of a text view
54 * within a larger container.
57 public class TwoDScrollView extends FrameLayout {
58 static final int ANIMATED_SCROLL_GAP = 250;
59 static final float MAX_SCROLL_FACTOR = 0.5f;
61 private long mLastScroll;
63 private final Rect mTempRect = new Rect();
64 private Scroller mScroller;
67 * Flag to indicate that we are moving focus ourselves. This is so the
68 * code that watches for focus changes initiated outside this TwoDScrollView
69 * knows that it does not have to do anything.
71 private boolean mTwoDScrollViewMovedFocus;
74 * Position of the last motion event.
76 private float mLastMotionY;
77 private float mLastMotionX;
80 * True when the layout has changed but the traversal has not come through yet.
81 * Ideally the view hierarchy would keep track of this for us.
83 private boolean mIsLayoutDirty = true;
86 * The child to give focus to in the event that a child has requested focus while the
87 * layout is dirty. This prevents the scroll from being wrong if the child has not been
88 * laid out before requesting focus.
90 private View mChildToScrollTo = null;
93 * True if the user is currently dragging this TwoDScrollView around. This is
94 * not the same as 'is being flinged', which can be checked by
95 * mScroller.isFinished() (flinging begins when the user lifts his finger).
97 private boolean mIsBeingDragged = false;
100 * Determines speed during touch scrolling
102 private VelocityTracker mVelocityTracker;
105 * Whether arrow scrolling is animated.
107 private int mTouchSlop;
108 private int mMinimumVelocity;
109 private int mMaximumVelocity;
111 public TwoDScrollView(Context context) {
113 initTwoDScrollView();
116 public TwoDScrollView(Context context, AttributeSet attrs) {
117 super(context, attrs);
118 initTwoDScrollView();
121 public TwoDScrollView(Context context, AttributeSet attrs, int defStyle) {
122 super(context, attrs, defStyle);
123 initTwoDScrollView();
127 protected float getTopFadingEdgeStrength() {
128 if (getChildCount() == 0) {
131 final int length = getVerticalFadingEdgeLength();
132 if (getScrollY() < length) {
133 return getScrollY() / (float) length;
139 protected float getBottomFadingEdgeStrength() {
140 if (getChildCount() == 0) {
143 final int length = getVerticalFadingEdgeLength();
144 final int bottomEdge = getHeight() - getPaddingBottom();
145 final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
147 return span / (float) length;
153 protected float getLeftFadingEdgeStrength() {
154 if (getChildCount() == 0) {
157 final int length = getHorizontalFadingEdgeLength();
158 if (getScrollX() < length) {
159 return getScrollX() / (float) length;
165 protected float getRightFadingEdgeStrength() {
166 if (getChildCount() == 0) {
169 final int length = getHorizontalFadingEdgeLength();
170 final int rightEdge = getWidth() - getPaddingRight();
171 final int span = getChildAt(0).getRight() - getScrollX() - rightEdge;
173 return span / (float) length;
179 * @return The maximum amount this scroll view will scroll in response to
182 public int getMaxScrollAmountVertical() {
183 return (int) (MAX_SCROLL_FACTOR * getHeight());
185 public int getMaxScrollAmountHorizontal() {
186 return (int) (MAX_SCROLL_FACTOR * getWidth());
189 private void initTwoDScrollView() {
190 mScroller = new Scroller(getContext());
192 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
193 setWillNotDraw(false);
194 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
195 mTouchSlop = configuration.getScaledTouchSlop();
196 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
197 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
201 public void addView(View child) {
202 if (getChildCount() > 0) {
203 throw new IllegalStateException("TwoDScrollView can host only one direct child");
205 super.addView(child);
209 public void addView(View child, int index) {
210 if (getChildCount() > 0) {
211 throw new IllegalStateException("TwoDScrollView can host only one direct child");
213 super.addView(child, index);
217 public void addView(View child, ViewGroup.LayoutParams params) {
218 if (getChildCount() > 0) {
219 throw new IllegalStateException("TwoDScrollView can host only one direct child");
221 super.addView(child, params);
225 public void addView(View child, int index, ViewGroup.LayoutParams params) {
226 if (getChildCount() > 0) {
227 throw new IllegalStateException("TwoDScrollView can host only one direct child");
229 super.addView(child, index, params);
233 * @return Returns true this TwoDScrollView can be scrolled
235 private boolean canScroll() {
236 View child = getChildAt(0);
238 int childHeight = child.getHeight();
239 int childWidth = child.getWidth();
240 return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) ||
241 (getWidth() < childWidth + getPaddingLeft() + getPaddingRight());
247 public boolean dispatchKeyEvent(KeyEvent event) {
248 // Let the focused view and/or our descendants get the key first
249 boolean handled = super.dispatchKeyEvent(event);
253 return executeKeyEvent(event);
257 * You can call this function yourself to have the scroll view perform
258 * scrolling from a key event, just as if the event had been dispatched to
259 * it by the view hierarchy.
261 * @param event The key event to execute.
262 * @return Return true if the event was handled, else false.
264 public boolean executeKeyEvent(KeyEvent event) {
265 mTempRect.setEmpty();
268 View currentFocused = findFocus();
269 if (currentFocused == this) currentFocused = null;
270 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN);
271 return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN);
275 boolean handled = false;
276 if (event.getAction() == KeyEvent.ACTION_DOWN) {
277 switch (event.getKeyCode()) {
278 case KeyEvent.KEYCODE_DPAD_UP:
279 if (!event.isAltPressed()) {
280 handled = arrowScroll(View.FOCUS_UP, false);
282 handled = fullScroll(View.FOCUS_UP, false);
285 case KeyEvent.KEYCODE_DPAD_DOWN:
286 if (!event.isAltPressed()) {
287 handled = arrowScroll(View.FOCUS_DOWN, false);
289 handled = fullScroll(View.FOCUS_DOWN, false);
292 case KeyEvent.KEYCODE_DPAD_LEFT:
293 if (!event.isAltPressed()) {
294 handled = arrowScroll(View.FOCUS_LEFT, true);
296 handled = fullScroll(View.FOCUS_LEFT, true);
299 case KeyEvent.KEYCODE_DPAD_RIGHT:
300 if (!event.isAltPressed()) {
301 handled = arrowScroll(View.FOCUS_RIGHT, true);
303 handled = fullScroll(View.FOCUS_RIGHT, true);
312 public boolean onInterceptTouchEvent(MotionEvent ev) {
314 * This method JUST determines whether we want to intercept the motion.
315 * If we return true, onMotionEvent will be called and we do the actual
318 * Shortcut the most recurring case: the user is in the dragging
319 * state and he is moving his finger. We want to intercept this
322 final int action = ev.getAction();
323 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
327 mIsBeingDragged = false;
330 final float y = ev.getY();
331 final float x = ev.getX();
333 case MotionEvent.ACTION_MOVE:
335 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
336 * whether the user has moved far enough from his original down touch.
339 * Locally do absolute value. mLastMotionY is set to the y value
342 final int yDiff = (int) Math.abs(y - mLastMotionY);
343 final int xDiff = (int) Math.abs(x - mLastMotionX);
344 if (yDiff > mTouchSlop || xDiff > mTouchSlop) {
345 mIsBeingDragged = true;
349 case MotionEvent.ACTION_DOWN:
350 /* Remember location of down touch */
355 * If being flinged and user touches the screen, initiate drag;
356 * otherwise don't. mScroller.isFinished should be false when
359 mIsBeingDragged = !mScroller.isFinished();
362 case MotionEvent.ACTION_CANCEL:
363 case MotionEvent.ACTION_UP:
364 /* Release the drag */
365 mIsBeingDragged = false;
370 * The only time we want to intercept motion events is if we are in the
373 return mIsBeingDragged;
377 public boolean onTouchEvent(MotionEvent ev) {
379 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
380 // Don't handle edge touches immediately -- they may actually belong to one of our
389 if (mVelocityTracker == null) {
390 mVelocityTracker = VelocityTracker.obtain();
392 mVelocityTracker.addMovement(ev);
394 final int action = ev.getAction();
395 final float y = ev.getY();
396 final float x = ev.getX();
399 case MotionEvent.ACTION_DOWN:
401 * If being flinged and user touches, stop the fling. isFinished
402 * will be false if being flinged.
404 if (!mScroller.isFinished()) {
405 mScroller.abortAnimation();
408 // Remember where the motion event started
412 case MotionEvent.ACTION_MOVE:
413 // Scroll to follow the motion event
414 int deltaX = (int) (mLastMotionX - x);
415 int deltaY = (int) (mLastMotionY - y);
420 if (getScrollX() < 0) {
423 } else if (deltaX > 0) {
424 final int rightEdge = getWidth() - getPaddingRight();
425 final int availableToScroll = getChildAt(0).getRight() - getScrollX() - rightEdge;
426 if (availableToScroll > 0) {
427 deltaX = Math.min(availableToScroll, deltaX);
433 if (getScrollY() < 0) {
436 } else if (deltaY > 0) {
437 final int bottomEdge = getHeight() - getPaddingBottom();
438 final int availableToScroll = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
439 if (availableToScroll > 0) {
440 deltaY = Math.min(availableToScroll, deltaY);
445 if (deltaY != 0 || deltaX != 0)
446 scrollBy(deltaX, deltaY);
448 case MotionEvent.ACTION_UP:
449 final VelocityTracker velocityTracker = mVelocityTracker;
450 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
451 int initialXVelocity = (int) velocityTracker.getXVelocity();
452 int initialYVelocity = (int) velocityTracker.getYVelocity();
453 if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) && getChildCount() > 0) {
454 fling(-initialXVelocity, -initialYVelocity);
456 if (mVelocityTracker != null) {
457 mVelocityTracker.recycle();
458 mVelocityTracker = null;
465 * Finds the next focusable component that fits in this View's bounds
466 * (excluding fading edges) pretending that this View's top is located at
469 * @param topFocus look for a candidate is the one at the top of the bounds
470 * if topFocus is true, or at the bottom of the bounds if topFocus is
472 * @param top the top offset of the bounds in which a focusable must be
473 * found (the fading edge is assumed to start at this position)
474 * @param preferredFocusable the View that has highest priority and will be
475 * returned if it is within my bounds (null is valid)
476 * @return the next focusable component in the bounds or null if none can be
479 private View findFocusableViewInMyBounds(final boolean topFocus, final int top, final boolean leftFocus, final int left, View preferredFocusable) {
481 * The fading edge's transparent side should be considered for focus
482 * since it's mostly visible, so we divide the actual fading edge length
485 final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2;
486 final int topWithoutFadingEdge = top + verticalFadingEdgeLength;
487 final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength;
488 final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
489 final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength;
490 final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength;
492 if ((preferredFocusable != null)
493 && (preferredFocusable.getTop() < bottomWithoutFadingEdge)
494 && (preferredFocusable.getBottom() > topWithoutFadingEdge)
495 && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
496 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
497 return preferredFocusable;
499 return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge, leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
503 * Finds the next focusable component that fits in the specified bounds.
506 * @param topFocus look for a candidate is the one at the top of the bounds
507 * if topFocus is true, or at the bottom of the bounds if topFocus is
509 * @param top the top offset of the bounds in which a focusable must be
511 * @param bottom the bottom offset of the bounds in which a focusable must
513 * @return the next focusable component in the bounds or null if none can
516 private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus, int left, int right) {
517 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
518 View focusCandidate = null;
521 * A fully contained focusable is one where its top is below the bound's
522 * top, and its bottom is above the bound's bottom. A partially
523 * contained focusable is one where some part of it is within the
524 * bounds, but it also has some part that is not within bounds. A fully contained
525 * focusable is preferred to a partially contained focusable.
527 boolean foundFullyContainedFocusable = false;
529 int count = focusables.size();
530 for (int i = 0; i < count; i++) {
531 View view = focusables.get(i);
532 int viewTop = view.getTop();
533 int viewBottom = view.getBottom();
534 int viewLeft = view.getLeft();
535 int viewRight = view.getRight();
537 if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right) {
539 * the focusable is in the target area, it is a candidate for
542 final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) && (left < viewLeft) && (viewRight < right);
543 if (focusCandidate == null) {
544 /* No candidate, take this one */
545 focusCandidate = view;
546 foundFullyContainedFocusable = viewIsFullyContained;
548 final boolean viewIsCloserToVerticalBoundary =
549 (topFocus && viewTop < focusCandidate.getTop()) ||
550 (!topFocus && viewBottom > focusCandidate.getBottom());
551 final boolean viewIsCloserToHorizontalBoundary =
552 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
553 (!leftFocus && viewRight > focusCandidate.getRight());
554 if (foundFullyContainedFocusable) {
555 if (viewIsFullyContained && viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) {
557 * We're dealing with only fully contained views, so
558 * it has to be closer to the boundary to beat our
561 focusCandidate = view;
564 if (viewIsFullyContained) {
565 /* Any fully contained view beats a partially contained view */
566 focusCandidate = view;
567 foundFullyContainedFocusable = true;
568 } else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) {
570 * Partially contained view beats another partially
571 * contained view if it's closer
573 focusCandidate = view;
579 return focusCandidate;
583 * <p>Handles scrolling in response to a "home/end" shortcut press. This
584 * method will scroll the view to the top or bottom and give the focus
585 * to the topmost/bottommost component in the new visible area. If no
586 * component is a good candidate for focus, this scrollview reclaims the
589 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
590 * to go the top of the view or
591 * {@link android.view.View#FOCUS_DOWN} to go the bottom
592 * @return true if the key event is consumed by this method, false otherwise
594 public boolean fullScroll(int direction, boolean horizontal) {
596 boolean down = direction == View.FOCUS_DOWN;
597 int height = getHeight();
599 mTempRect.bottom = height;
601 int count = getChildCount();
603 View view = getChildAt(count - 1);
604 mTempRect.bottom = view.getBottom();
605 mTempRect.top = mTempRect.bottom - height;
608 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0);
610 boolean right = direction == View.FOCUS_DOWN;
611 int width = getWidth();
613 mTempRect.right = width;
615 int count = getChildCount();
617 View view = getChildAt(count - 1);
618 mTempRect.right = view.getBottom();
619 mTempRect.left = mTempRect.right - width;
622 return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom);
627 * <p>Scrolls the view to make the area defined by <code>top</code> and
628 * <code>bottom</code> visible. This method attempts to give the focus
629 * to a component visible in this area. If no component can be focused in
630 * the new visible area, the focus is reclaimed by this scrollview.</p>
632 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
634 * {@link android.view.View#FOCUS_DOWN} to downward
635 * @param top the top offset of the new area to be made visible
636 * @param bottom the bottom offset of the new area to be made visible
637 * @return true if the key event is consumed by this method, false otherwise
639 private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left, int right) {
640 boolean handled = true;
641 int height = getHeight();
642 int containerTop = getScrollY();
643 int containerBottom = containerTop + height;
644 boolean up = directionY == View.FOCUS_UP;
645 int width = getWidth();
646 int containerLeft = getScrollX();
647 int containerRight = containerLeft + width;
648 boolean leftwards = directionX == View.FOCUS_UP;
649 View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right);
650 if (newFocused == null) {
653 if ((top >= containerTop && bottom <= containerBottom) || (left >= containerLeft && right <= containerRight)) {
656 int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
657 int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
658 doScroll(deltaX, deltaY);
660 if (newFocused != findFocus() && newFocused.requestFocus(directionY)) {
661 mTwoDScrollViewMovedFocus = true;
662 mTwoDScrollViewMovedFocus = false;
668 * Handle scrolling in response to an up or down arrow click.
670 * @param direction The direction corresponding to the arrow key that was
672 * @return True if we consumed the event, false otherwise
674 public boolean arrowScroll(int direction, boolean horizontal) {
675 View currentFocused = findFocus();
676 if (currentFocused == this) currentFocused = null;
677 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
678 final int maxJump = horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical();
681 if (nextFocused != null) {
682 nextFocused.getDrawingRect(mTempRect);
683 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
684 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
685 doScroll(0, scrollDelta);
686 nextFocused.requestFocus(direction);
689 int scrollDelta = maxJump;
690 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
691 scrollDelta = getScrollY();
692 } else if (direction == View.FOCUS_DOWN) {
693 if (getChildCount() > 0) {
694 int daBottom = getChildAt(0).getBottom();
695 int screenBottom = getScrollY() + getHeight();
696 if (daBottom - screenBottom < maxJump) {
697 scrollDelta = daBottom - screenBottom;
701 if (scrollDelta == 0) {
704 doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
707 if (nextFocused != null) {
708 nextFocused.getDrawingRect(mTempRect);
709 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
710 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
711 doScroll(scrollDelta, 0);
712 nextFocused.requestFocus(direction);
715 int scrollDelta = maxJump;
716 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
717 scrollDelta = getScrollY();
718 } else if (direction == View.FOCUS_DOWN) {
719 if (getChildCount() > 0) {
720 int daBottom = getChildAt(0).getBottom();
721 int screenBottom = getScrollY() + getHeight();
722 if (daBottom - screenBottom < maxJump) {
723 scrollDelta = daBottom - screenBottom;
727 if (scrollDelta == 0) {
730 doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
737 * Smooth scroll by a Y delta
739 * @param delta the number of pixels to scroll by on the Y axis
741 private void doScroll(int deltaX, int deltaY) {
742 if (deltaX != 0 || deltaY != 0) {
743 smoothScrollBy(deltaX, deltaY);
748 * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
750 * @param dx the number of pixels to scroll by on the X axis
751 * @param dy the number of pixels to scroll by on the Y axis
753 public final void smoothScrollBy(int dx, int dy) {
754 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
755 if (duration > ANIMATED_SCROLL_GAP) {
756 mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
757 awakenScrollBars(mScroller.getDuration());
760 if (!mScroller.isFinished()) {
761 mScroller.abortAnimation();
765 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
769 * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
771 * @param x the position where to scroll on the X axis
772 * @param y the position where to scroll on the Y axis
774 public final void smoothScrollTo(int x, int y) {
775 smoothScrollBy(x - getScrollX(), y - getScrollY());
779 * <p>The scroll range of a scroll view is the overall height of all of its
783 protected int computeVerticalScrollRange() {
784 int count = getChildCount();
785 return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
788 protected int computeHorizontalScrollRange() {
789 int count = getChildCount();
790 return count == 0 ? getWidth() : (getChildAt(0)).getRight();
794 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
795 ViewGroup.LayoutParams lp = child.getLayoutParams();
796 int childWidthMeasureSpec;
797 int childHeightMeasureSpec;
799 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width);
800 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
802 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
806 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
807 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
808 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
809 getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
810 final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
812 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
816 public void computeScroll() {
817 if (mScroller.computeScrollOffset()) {
818 // This is called at drawing time by ViewGroup. We don't want to
819 // re-show the scrollbars at this point, which scrollTo will do,
820 // so we replicate most of scrollTo here.
822 // It's a little odd to call onScrollChanged from inside the drawing.
824 // It is, except when you remember that computeScroll() is used to
825 // animate scrolling. So unless we want to defer the onScrollChanged()
826 // until the end of the animated scrolling, we don't really have a
829 // I agree. The alternative, which I think would be worse, is to post
830 // something and tell the subclasses later. This is bad because there
831 // will be a window where mScrollX/Y is different from what the app
834 int oldX = getScrollX();
835 int oldY = getScrollY();
836 int x = mScroller.getCurrX();
837 int y = mScroller.getCurrY();
838 if (getChildCount() > 0) {
839 View child = getChildAt(0);
840 scrollTo(clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()),
841 clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()));
845 if (oldX != getScrollX() || oldY != getScrollY()) {
846 onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
849 // Keep on drawing until the animation has finished.
855 * Scrolls the view to the given child.
857 * @param child the View to scroll to
859 private void scrollToChild(View child) {
860 child.getDrawingRect(mTempRect);
861 /* Offset from child's local coordinates to TwoDScrollView coordinates */
862 offsetDescendantRectToMyCoords(child, mTempRect);
863 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
864 if (scrollDelta != 0) {
865 scrollBy(0, scrollDelta);
870 * If rect is off screen, scroll just enough to get it (or at least the
871 * first screen size chunk of it) on screen.
873 * @param rect The rectangle.
874 * @param immediate True to scroll immediately without animation
875 * @return true if scrolling was performed
877 private boolean scrollToChildRect(Rect rect, boolean immediate) {
878 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
879 final boolean scroll = delta != 0;
884 smoothScrollBy(0, delta);
891 * Compute the amount to scroll in the Y direction in order to get
892 * a rectangle completely on the screen (or, if taller than the screen,
893 * at least the first screen size chunk of it).
895 * @param rect The rect.
896 * @return The scroll delta.
898 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
899 if (getChildCount() == 0) return 0;
900 int height = getHeight();
901 int screenTop = getScrollY();
902 int screenBottom = screenTop + height;
903 int fadingEdge = getVerticalFadingEdgeLength();
904 // leave room for top fading edge as long as rect isn't at very top
906 screenTop += fadingEdge;
909 // leave room for bottom fading edge as long as rect isn't at very bottom
910 if (rect.bottom < getChildAt(0).getHeight()) {
911 screenBottom -= fadingEdge;
913 int scrollYDelta = 0;
914 if (rect.bottom > screenBottom && rect.top > screenTop) {
915 // need to move down to get it in view: move down just enough so
916 // that the entire rectangle is in view (or at least the first
917 // screen size chunk).
918 if (rect.height() > height) {
919 // just enough to get screen size chunk on
920 scrollYDelta += (rect.top - screenTop);
922 // get entire rect at bottom of screen
923 scrollYDelta += (rect.bottom - screenBottom);
926 // make sure we aren't scrolling beyond the end of our content
927 int bottom = getChildAt(0).getBottom();
928 int distanceToBottom = bottom - screenBottom;
929 scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
931 } else if (rect.top < screenTop && rect.bottom < screenBottom) {
932 // need to move up to get it in view: move up just enough so that
933 // entire rectangle is in view (or at least the first screen
934 // size chunk of it).
936 if (rect.height() > height) {
938 scrollYDelta -= (screenBottom - rect.bottom);
940 // entire rect at top
941 scrollYDelta -= (screenTop - rect.top);
944 // make sure we aren't scrolling any further than the top our content
945 scrollYDelta = Math.max(scrollYDelta, -getScrollY());
951 public void requestChildFocus(View child, View focused) {
952 if (!mTwoDScrollViewMovedFocus) {
953 if (!mIsLayoutDirty) {
954 scrollToChild(focused);
956 // The child may not be laid out yet, we can't compute the scroll yet
957 mChildToScrollTo = focused;
960 super.requestChildFocus(child, focused);
964 * When looking for focus in children of a scroll view, need to be a little
965 * more careful not to give focus to something that is scrolled off screen.
967 * This is more expensive than the default {@link android.view.ViewGroup}
968 * implementation, otherwise this behavior might have been made the default.
971 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
972 // convert from forward / backward notation to up / down / left / right
974 if (direction == View.FOCUS_FORWARD) {
975 direction = View.FOCUS_DOWN;
976 } else if (direction == View.FOCUS_BACKWARD) {
977 direction = View.FOCUS_UP;
980 final View nextFocus = previouslyFocusedRect == null ?
981 FocusFinder.getInstance().findNextFocus(this, null, direction) :
982 FocusFinder.getInstance().findNextFocusFromRect(this,
983 previouslyFocusedRect, direction);
985 if (nextFocus == null) {
989 return nextFocus.requestFocus(direction, previouslyFocusedRect);
993 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
994 // offset into coordinate space of this scroll view
995 rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
996 return scrollToChildRect(rectangle, immediate);
1000 public void requestLayout() {
1001 mIsLayoutDirty = true;
1002 super.requestLayout();
1006 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1007 super.onLayout(changed, l, t, r, b);
1008 mIsLayoutDirty = false;
1009 // Give a child focus if it needs it
1010 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
1011 scrollToChild(mChildToScrollTo);
1013 mChildToScrollTo = null;
1015 // Calling this with the present values causes it to re-clam them
1016 scrollTo(getScrollX(), getScrollY());
1020 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1021 super.onSizeChanged(w, h, oldw, oldh);
1023 View currentFocused = findFocus();
1024 if (null == currentFocused || this == currentFocused)
1027 // If the currently-focused view was visible on the screen when the
1028 // screen was at the old height, then scroll the screen to make that
1029 // view visible with the new screen height.
1030 currentFocused.getDrawingRect(mTempRect);
1031 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1032 int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1033 int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1034 doScroll(scrollDeltaX, scrollDeltaY);
1038 * Return true if child is an descendant of parent, (or equal to the parent).
1040 private boolean isViewDescendantOf(View child, View parent) {
1041 if (child == parent) {
1045 final ViewParent theParent = child.getParent();
1046 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1050 * Fling the scroll view
1052 * @param velocityY The initial velocity in the Y direction. Positive
1053 * numbers mean that the finger/curor is moving down the screen,
1054 * which means we want to scroll towards the top.
1056 public void fling(int velocityX, int velocityY) {
1057 if (getChildCount() > 0) {
1058 int height = getHeight() - getPaddingBottom() - getPaddingTop();
1059 int bottom = getChildAt(0).getHeight();
1060 int width = getWidth() - getPaddingRight() - getPaddingLeft();
1061 int right = getChildAt(0).getWidth();
1063 mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0, bottom - height);
1065 final boolean movingDown = velocityY > 0;
1066 final boolean movingRight = velocityX > 0;
1068 View newFocused = findFocusableViewInMyBounds(movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus());
1069 if (newFocused == null) {
1073 if (newFocused != findFocus() && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) {
1074 mTwoDScrollViewMovedFocus = true;
1075 mTwoDScrollViewMovedFocus = false;
1078 awakenScrollBars(mScroller.getDuration());
1086 * <p>This version also clamps the scrolling to the bounds of our child.
1088 public void scrollTo(int x, int y) {
1089 // we rely on the fact the View.scrollBy calls scrollTo.
1090 if (getChildCount() > 0) {
1091 View child = getChildAt(0);
1092 x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
1093 y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
1094 if (x != getScrollX() || y != getScrollY()) {
1095 super.scrollTo(x, y);
1100 private int clamp(int n, int my, int child) {
1101 if (my >= child || n < 0) {
1102 /* my >= child is this case:
1103 * |--------------- me ---------------|
1104 * |------ child ------|
1106 * |--------------- me ---------------|
1107 * |------ child ------|
1109 * |--------------- me ---------------|
1110 * |------ child ------|
1112 * n < 0 is this case:
1113 * |------ me ------|
1114 * |-------- child --------|
1119 if ((my+n) > child) {
1121 * |------ me ------|
1122 * |------ child ------|