59 static final int ANIMATED_SCROLL_GAP = 250;
60 static final float MAX_SCROLL_FACTOR = 0.5f;
61 private final Rect mTempRect =
new Rect();
63 private long mLastScroll;
64 private Scroller mScroller;
65 private boolean scrollEnabled =
true;
71 private boolean mTwoDScrollViewMovedFocus;
75 private float mLastMotionY;
76 private float mLastMotionX;
81 private boolean mIsLayoutDirty =
true;
87 private View mChildToScrollTo =
null;
93 private boolean mIsBeingDragged =
false;
97 private VelocityTracker mVelocityTracker;
101 private int mTouchSlop;
102 private int mMinimumVelocity;
103 private int mMaximumVelocity;
107 initTwoDScrollView();
110 public ScrollView2D(Context context, AttributeSet attrs)
112 super(context, attrs);
113 initTwoDScrollView();
116 public ScrollView2D(Context context, AttributeSet attrs,
int defStyle)
118 super(context, attrs, defStyle);
119 initTwoDScrollView();
122 @Override
protected float getTopFadingEdgeStrength()
124 if (getChildCount() == 0)
128 final int length = getVerticalFadingEdgeLength();
129 if (getScrollY() < length)
131 return getScrollY() / (float)length;
136 @Override
protected float getBottomFadingEdgeStrength()
138 if (getChildCount() == 0)
142 final int length = getVerticalFadingEdgeLength();
143 final int bottomEdge = getHeight() - getPaddingBottom();
144 final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
147 return span / (float)length;
152 @Override
protected float getLeftFadingEdgeStrength()
154 if (getChildCount() == 0)
158 final int length = getHorizontalFadingEdgeLength();
159 if (getScrollX() < length)
161 return getScrollX() / (float)length;
166 @Override
protected float getRightFadingEdgeStrength()
168 if (getChildCount() == 0)
172 final int length = getHorizontalFadingEdgeLength();
173 final int rightEdge = getWidth() - getPaddingRight();
174 final int span = getChildAt(0).getRight() - getScrollX() - rightEdge;
177 return span / (float)length;
187 scrollEnabled = enable;
196 return (
int)(MAX_SCROLL_FACTOR * getHeight());
199 public int getMaxScrollAmountHorizontal()
201 return (
int)(MAX_SCROLL_FACTOR * getWidth());
204 private void initTwoDScrollView()
206 mScroller =
new Scroller(getContext());
208 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
209 setWillNotDraw(
false);
210 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
211 mTouchSlop = configuration.getScaledTouchSlop();
212 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
213 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
216 @Override
public void addView(View child)
218 if (getChildCount() > 0)
220 throw new IllegalStateException(
"TwoDScrollView can host only one direct child");
222 super.addView(child);
225 @Override
public void addView(View child,
int index)
227 if (getChildCount() > 0)
229 throw new IllegalStateException(
"TwoDScrollView can host only one direct child");
231 super.addView(child, index);
234 @Override
public void addView(View child, ViewGroup.LayoutParams params)
236 if (getChildCount() > 0)
238 throw new IllegalStateException(
"TwoDScrollView can host only one direct child");
240 super.addView(child, params);
243 @Override
public void addView(View child,
int index, ViewGroup.LayoutParams params)
245 if (getChildCount() > 0)
247 throw new IllegalStateException(
"TwoDScrollView can host only one direct child");
249 super.addView(child, index, params);
255 private boolean canScroll()
259 View child = getChildAt(0);
262 int childHeight = child.getHeight();
263 int childWidth = child.getWidth();
264 return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) ||
265 (getWidth() < childWidth + getPaddingLeft() + getPaddingRight());
270 @Override
public boolean onInterceptTouchEvent(MotionEvent ev)
281 final int action = ev.getAction();
282 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged))
288 mIsBeingDragged =
false;
291 final float y = ev.getY();
292 final float x = ev.getX();
295 case MotionEvent.ACTION_MOVE:
304 final int yDiff = (int)Math.abs(y - mLastMotionY);
305 final int xDiff = (int)Math.abs(x - mLastMotionX);
306 if (yDiff > mTouchSlop || xDiff > mTouchSlop)
308 mIsBeingDragged =
true;
312 case MotionEvent.ACTION_DOWN:
322 mIsBeingDragged = !mScroller.isFinished();
325 case MotionEvent.ACTION_CANCEL:
326 case MotionEvent.ACTION_UP:
328 mIsBeingDragged =
false;
336 return mIsBeingDragged;
339 @Override
public boolean onTouchEvent(MotionEvent ev)
342 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0)
354 if (mVelocityTracker ==
null)
356 mVelocityTracker = VelocityTracker.obtain();
358 mVelocityTracker.addMovement(ev);
360 final int action = ev.getAction();
361 final float y = ev.getY();
362 final float x = ev.getX();
366 case MotionEvent.ACTION_DOWN:
371 if (!mScroller.isFinished())
373 mScroller.abortAnimation();
380 case MotionEvent.ACTION_MOVE:
382 int deltaX = (int)(mLastMotionX - x);
383 int deltaY = (int)(mLastMotionY - y);
389 if (getScrollX() < 0)
396 final int rightEdge = getWidth() - getPaddingRight();
397 final int availableToScroll =
398 getChildAt(0).getRight() - getScrollX() - rightEdge;
399 if (availableToScroll > 0)
401 deltaX = Math.min(availableToScroll, deltaX);
410 if (getScrollY() < 0)
417 final int bottomEdge = getHeight() - getPaddingBottom();
418 final int availableToScroll =
419 getChildAt(0).getBottom() - getScrollY() - bottomEdge;
420 if (availableToScroll > 0)
422 deltaY = Math.min(availableToScroll, deltaY);
429 if (deltaY != 0 || deltaX != 0)
430 scrollBy(deltaX, deltaY);
432 case MotionEvent.ACTION_UP:
433 final VelocityTracker velocityTracker = mVelocityTracker;
434 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
435 int initialXVelocity = (int)velocityTracker.getXVelocity();
436 int initialYVelocity = (int)velocityTracker.getYVelocity();
437 if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) &&
440 fling(-initialXVelocity, -initialYVelocity);
442 if (mVelocityTracker !=
null)
444 mVelocityTracker.recycle();
445 mVelocityTracker =
null;
466 private View findFocusableViewInMyBounds(
final boolean topFocus,
final int top,
467 final boolean leftFocus,
final int left,
468 View preferredFocusable)
475 final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2;
476 final int topWithoutFadingEdge = top + verticalFadingEdgeLength;
477 final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength;
478 final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
479 final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength;
480 final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength;
482 if ((preferredFocusable !=
null) &&
483 (preferredFocusable.getTop() < bottomWithoutFadingEdge) &&
484 (preferredFocusable.getBottom() > topWithoutFadingEdge) &&
485 (preferredFocusable.getLeft() < rightWithoutFadingEdge) &&
486 (preferredFocusable.getRight() > leftWithoutFadingEdge))
488 return preferredFocusable;
490 return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge,
491 leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
508 private View findFocusableViewInBounds(
boolean topFocus,
int top,
int bottom,
boolean leftFocus,
511 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
512 View focusCandidate =
null;
521 boolean foundFullyContainedFocusable =
false;
523 int count = focusables.size();
524 for (
int i = 0; i < count; i++)
526 View view = focusables.get(i);
527 int viewTop = view.getTop();
528 int viewBottom = view.getBottom();
529 int viewLeft = view.getLeft();
530 int viewRight = view.getRight();
532 if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right)
538 final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) &&
539 (left < viewLeft) && (viewRight < right);
540 if (focusCandidate ==
null)
543 focusCandidate = view;
544 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)
556 if (viewIsFullyContained && viewIsCloserToVerticalBoundary &&
557 viewIsCloserToHorizontalBoundary)
564 focusCandidate = view;
569 if (viewIsFullyContained)
572 focusCandidate = view;
573 foundFullyContainedFocusable =
true;
575 else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary)
581 focusCandidate = view;
587 return focusCandidate;
606 boolean down = direction == View.FOCUS_DOWN;
607 int height = getHeight();
609 mTempRect.bottom = height;
612 int count = getChildCount();
615 View view = getChildAt(count - 1);
616 mTempRect.bottom = view.getBottom();
617 mTempRect.top = mTempRect.bottom - height;
620 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0);
624 boolean right = direction == View.FOCUS_DOWN;
625 int width = getWidth();
627 mTempRect.right = width;
630 int count = getChildCount();
633 View view = getChildAt(count - 1);
634 mTempRect.right = view.getBottom();
635 mTempRect.left = mTempRect.right - width;
638 return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom);
655 private boolean scrollAndFocus(
int directionY,
int top,
int bottom,
int directionX,
int left,
658 boolean handled =
true;
659 int height = getHeight();
660 int containerTop = getScrollY();
661 int containerBottom = containerTop + height;
662 boolean up = directionY == View.FOCUS_UP;
663 int width = getWidth();
664 int containerLeft = getScrollX();
665 int containerRight = containerLeft + width;
666 boolean leftwards = directionX == View.FOCUS_UP;
667 View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right);
668 if (newFocused ==
null)
672 if ((top >= containerTop && bottom <= containerBottom) ||
673 (left >= containerLeft && right <= containerRight))
679 int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
680 int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
681 doScroll(deltaX, deltaY);
683 if (newFocused != findFocus() && newFocused.requestFocus(directionY))
685 mTwoDScrollViewMovedFocus =
true;
686 mTwoDScrollViewMovedFocus =
false;
700 View currentFocused = findFocus();
701 if (currentFocused ==
this)
702 currentFocused =
null;
703 View nextFocused = FocusFinder.getInstance().findNextFocus(
this, currentFocused, direction);
709 if (nextFocused !=
null)
711 nextFocused.getDrawingRect(mTempRect);
712 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
714 doScroll(0, scrollDelta);
715 nextFocused.requestFocus(direction);
720 int scrollDelta = maxJump;
721 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
723 scrollDelta = getScrollY();
725 else if (direction == View.FOCUS_DOWN)
727 if (getChildCount() > 0)
729 int daBottom = getChildAt(0).getBottom();
730 int screenBottom = getScrollY() + getHeight();
731 if (daBottom - screenBottom < maxJump)
733 scrollDelta = daBottom - screenBottom;
737 if (scrollDelta == 0)
741 doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
746 if (nextFocused !=
null)
748 nextFocused.getDrawingRect(mTempRect);
749 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
751 doScroll(scrollDelta, 0);
752 nextFocused.requestFocus(direction);
757 int scrollDelta = maxJump;
758 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
760 scrollDelta = getScrollY();
762 else if (direction == View.FOCUS_DOWN)
764 if (getChildCount() > 0)
766 int daBottom = getChildAt(0).getBottom();
767 int screenBottom = getScrollY() + getHeight();
768 if (daBottom - screenBottom < maxJump)
770 scrollDelta = daBottom - screenBottom;
774 if (scrollDelta == 0)
778 doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
789 private void doScroll(
int deltaX,
int deltaY)
791 if (deltaX != 0 || deltaY != 0)
805 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
806 if (duration > ANIMATED_SCROLL_GAP)
808 mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
809 awakenScrollBars(mScroller.getDuration());
814 if (!mScroller.isFinished())
816 mScroller.abortAnimation();
820 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
840 int count = getChildCount();
841 return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
844 @Override
protected int computeHorizontalScrollRange()
846 int count = getChildCount();
847 return count == 0 ? getWidth() : (getChildAt(0)).getRight();
851 protected void measureChild(View child,
int parentWidthMeasureSpec,
int parentHeightMeasureSpec)
853 ViewGroup.LayoutParams lp = child.getLayoutParams();
854 int childWidthMeasureSpec;
855 int childHeightMeasureSpec;
857 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
858 getPaddingLeft() + getPaddingRight(), lp.width);
859 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
861 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
865 protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec,
int widthUsed,
866 int parentHeightMeasureSpec,
int heightUsed)
868 final MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
869 final int childWidthMeasureSpec =
870 MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
871 final int childHeightMeasureSpec =
872 MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
874 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
877 @Override
public void computeScroll()
879 if (mScroller.computeScrollOffset())
897 int oldX = getScrollX();
898 int oldY = getScrollY();
899 int x = mScroller.getCurrX();
900 int y = mScroller.getCurrY();
901 if (getChildCount() > 0)
903 View child = getChildAt(0);
905 clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()),
906 clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(),
913 if (oldX != getScrollX() || oldY != getScrollY())
915 onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
928 private void scrollToChild(View child)
930 child.getDrawingRect(mTempRect);
932 offsetDescendantRectToMyCoords(child, mTempRect);
934 if (scrollDelta != 0)
936 scrollBy(0, scrollDelta);
948 private boolean scrollToChildRect(Rect rect,
boolean immediate)
951 final boolean scroll = delta != 0;
976 if (getChildCount() == 0)
978 int height = getHeight();
979 int screenTop = getScrollY();
980 int screenBottom = screenTop + height;
981 int fadingEdge = getVerticalFadingEdgeLength();
985 screenTop += fadingEdge;
989 if (rect.bottom < getChildAt(0).getHeight())
991 screenBottom -= fadingEdge;
993 int scrollYDelta = 0;
994 if (rect.bottom > screenBottom && rect.top > screenTop)
999 if (rect.height() > height)
1002 scrollYDelta += (rect.top - screenTop);
1007 scrollYDelta += (rect.bottom - screenBottom);
1011 int bottom = getChildAt(0).getBottom();
1012 int distanceToBottom = bottom - screenBottom;
1013 scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
1015 else if (rect.top < screenTop && rect.bottom < screenBottom)
1021 if (rect.height() > height)
1024 scrollYDelta -= (screenBottom - rect.bottom);
1029 scrollYDelta -= (screenTop - rect.top);
1033 scrollYDelta = Math.max(scrollYDelta, -getScrollY());
1035 return scrollYDelta;
1038 @Override
public void requestChildFocus(View child, View focused)
1040 if (!mTwoDScrollViewMovedFocus)
1042 if (!mIsLayoutDirty)
1044 scrollToChild(focused);
1049 mChildToScrollTo = focused;
1052 super.requestChildFocus(child, focused);
1067 if (direction == View.FOCUS_FORWARD)
1069 direction = View.FOCUS_DOWN;
1071 else if (direction == View.FOCUS_BACKWARD)
1073 direction = View.FOCUS_UP;
1076 final View nextFocus = previouslyFocusedRect ==
null
1077 ? FocusFinder.getInstance().findNextFocus(
this,
null, direction)
1078 : FocusFinder.getInstance().findNextFocusFromRect(
1079 this, previouslyFocusedRect, direction);
1081 if (nextFocus ==
null)
1086 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1090 public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
boolean immediate)
1093 rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
1094 return scrollToChildRect(rectangle, immediate);
1097 @Override
public void requestLayout()
1099 mIsLayoutDirty =
true;
1100 super.requestLayout();
1103 @Override
protected void onLayout(
boolean changed,
int l,
int t,
int r,
int b)
1105 super.onLayout(changed, l, t, r, b);
1106 mIsLayoutDirty =
false;
1108 if (mChildToScrollTo !=
null && isViewDescendantOf(mChildToScrollTo,
this))
1110 scrollToChild(mChildToScrollTo);
1112 mChildToScrollTo =
null;
1116 if (getChildCount() > 0)
1118 View child = getChildAt(0);
1119 int ptw = 0, pth = 0;
1120 if (child instanceof SessionView)
1122 ptw = ((SessionView)child).getTouchPointerPaddingWidth();
1123 pth = ((SessionView)child).getTouchPointerPaddingHeight();
1125 int contentW = child.getMeasuredWidth() - ptw;
1126 int contentH = child.getMeasuredHeight() - pth;
1127 int usableW = getWidth() - getPaddingLeft() - getPaddingRight();
1128 int usableH = getHeight() - getPaddingTop() - getPaddingBottom();
1129 int left = getPaddingLeft() + Math.max(0, (usableW - contentW) / 2);
1130 int top = getPaddingTop() + Math.max(0, (usableH - contentH) / 2);
1131 child.layout(left, top, left + child.getMeasuredWidth(),
1132 top + child.getMeasuredHeight());
1136 scrollTo(getScrollX(), getScrollY());
1139 @Override
protected void onSizeChanged(
int w,
int h,
int oldw,
int oldh)
1141 super.onSizeChanged(w, h, oldw, oldh);
1143 View currentFocused = findFocus();
1144 if (
null == currentFocused ||
this == currentFocused)
1150 currentFocused.getDrawingRect(mTempRect);
1151 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1154 doScroll(scrollDeltaX, scrollDeltaY);
1160 private boolean isViewDescendantOf(View child, View parent)
1162 if (child == parent)
1167 final ViewParent theParent = child.getParent();
1168 return (theParent instanceof ViewGroup) && isViewDescendantOf((View)theParent, parent);
1178 public void fling(
int velocityX,
int velocityY)
1180 if (getChildCount() > 0)
1182 int height = getHeight() - getPaddingBottom() - getPaddingTop();
1183 int bottom = getChildAt(0).getHeight();
1184 int width = getWidth() - getPaddingRight() - getPaddingLeft();
1185 int right = getChildAt(0).getWidth();
1187 mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0,
1190 final boolean movingDown = velocityY > 0;
1191 final boolean movingRight = velocityX > 0;
1193 View newFocused = findFocusableViewInMyBounds(
1194 movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus());
1195 if (newFocused ==
null)
1200 if (newFocused != findFocus() &&
1201 newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP))
1203 mTwoDScrollViewMovedFocus =
true;
1204 mTwoDScrollViewMovedFocus =
false;
1207 awakenScrollBars(mScroller.getDuration());
1220 if (getChildCount() > 0)
1222 View child = getChildAt(0);
1223 x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
1224 y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
1225 if (x != getScrollX() || y != getScrollY())
1227 super.scrollTo(x, y);
1232 private int clamp(
int n,
int my,
int child)
1234 if (my >= child || n < 0)
1253 if ((my + n) > child)
1265 public void setScrollViewListener(ScrollView2DListener scrollViewListener)
1267 this.scrollView2DListener = scrollViewListener;
1270 @Override
protected void onScrollChanged(
int x,
int y,
int oldx,
int oldy)
1272 super.onScrollChanged(x, y, oldx, oldy);
1273 if (scrollView2DListener !=
null)
1275 scrollView2DListener.onScrollChanged(
this, x, y, oldx, oldy);
1281 void onScrollChanged(
ScrollView2D scrollView,
int x,
int y,
int oldx,
int oldy);