61 static final int ANIMATED_SCROLL_GAP = 250;
62 static final float MAX_SCROLL_FACTOR = 0.5f;
63 private final Rect mTempRect =
new Rect();
65 private long mLastScroll;
66 private Scroller mScroller;
67 private boolean scrollEnabled =
true;
73 private boolean mTwoDScrollViewMovedFocus;
77 private float mLastMotionY;
78 private float mLastMotionX;
83 private boolean mIsLayoutDirty =
true;
89 private View mChildToScrollTo =
null;
95 private boolean mIsBeingDragged =
false;
99 private VelocityTracker mVelocityTracker;
103 private int mTouchSlop;
104 private int mMinimumVelocity;
105 private int mMaximumVelocity;
109 initTwoDScrollView();
112 public ScrollView2D(Context context, AttributeSet attrs)
114 super(context, attrs);
115 initTwoDScrollView();
118 public ScrollView2D(Context context, AttributeSet attrs,
int defStyle)
120 super(context, attrs, defStyle);
121 initTwoDScrollView();
124 @Override
protected float getTopFadingEdgeStrength()
126 if (getChildCount() == 0)
130 final int length = getVerticalFadingEdgeLength();
131 if (getScrollY() < length)
133 return getScrollY() / (float)length;
138 @Override
protected float getBottomFadingEdgeStrength()
140 if (getChildCount() == 0)
144 final int length = getVerticalFadingEdgeLength();
145 final int bottomEdge = getHeight() - getPaddingBottom();
146 final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
149 return span / (float)length;
154 @Override
protected float getLeftFadingEdgeStrength()
156 if (getChildCount() == 0)
160 final int length = getHorizontalFadingEdgeLength();
161 if (getScrollX() < length)
163 return getScrollX() / (float)length;
168 @Override
protected float getRightFadingEdgeStrength()
170 if (getChildCount() == 0)
174 final int length = getHorizontalFadingEdgeLength();
175 final int rightEdge = getWidth() - getPaddingRight();
176 final int span = getChildAt(0).getRight() - getScrollX() - rightEdge;
179 return span / (float)length;
189 scrollEnabled = enable;
198 return (
int)(MAX_SCROLL_FACTOR * getHeight());
201 public int getMaxScrollAmountHorizontal()
203 return (
int)(MAX_SCROLL_FACTOR * getWidth());
206 private void initTwoDScrollView()
208 mScroller =
new Scroller(getContext());
210 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
211 setWillNotDraw(
false);
212 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
213 mTouchSlop = configuration.getScaledTouchSlop();
214 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
215 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
218 @Override
public void addView(View child)
220 if (getChildCount() > 0)
222 throw new IllegalStateException(
"TwoDScrollView can host only one direct child");
224 super.addView(child);
227 @Override
public void addView(View child,
int index)
229 if (getChildCount() > 0)
231 throw new IllegalStateException(
"TwoDScrollView can host only one direct child");
233 super.addView(child, index);
236 @Override
public void addView(View child, ViewGroup.LayoutParams params)
238 if (getChildCount() > 0)
240 throw new IllegalStateException(
"TwoDScrollView can host only one direct child");
242 super.addView(child, params);
245 @Override
public void addView(View child,
int index, ViewGroup.LayoutParams params)
247 if (getChildCount() > 0)
249 throw new IllegalStateException(
"TwoDScrollView can host only one direct child");
251 super.addView(child, index, params);
257 private boolean canScroll()
261 View child = getChildAt(0);
264 int childHeight = child.getHeight();
265 int childWidth = child.getWidth();
266 return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) ||
267 (getWidth() < childWidth + getPaddingLeft() + getPaddingRight());
272 @Override
public boolean onInterceptTouchEvent(MotionEvent ev)
283 final int action = ev.getAction();
284 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged))
290 mIsBeingDragged =
false;
293 final float y = ev.getY();
294 final float x = ev.getX();
297 case MotionEvent.ACTION_MOVE:
306 final int yDiff = (int)Math.abs(y - mLastMotionY);
307 final int xDiff = (int)Math.abs(x - mLastMotionX);
308 if (yDiff > mTouchSlop || xDiff > mTouchSlop)
310 mIsBeingDragged =
true;
314 case MotionEvent.ACTION_DOWN:
324 mIsBeingDragged = !mScroller.isFinished();
327 case MotionEvent.ACTION_CANCEL:
328 case MotionEvent.ACTION_UP:
330 mIsBeingDragged =
false;
338 return mIsBeingDragged;
341 @Override
public boolean onTouchEvent(MotionEvent ev)
344 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0)
356 if (mVelocityTracker ==
null)
358 mVelocityTracker = VelocityTracker.obtain();
360 mVelocityTracker.addMovement(ev);
362 final int action = ev.getAction();
363 final float y = ev.getY();
364 final float x = ev.getX();
368 case MotionEvent.ACTION_DOWN:
373 if (!mScroller.isFinished())
375 mScroller.abortAnimation();
382 case MotionEvent.ACTION_MOVE:
384 int deltaX = (int)(mLastMotionX - x);
385 int deltaY = (int)(mLastMotionY - y);
391 if (getScrollX() < 0)
398 final int rightEdge = getWidth() - getPaddingRight();
399 final int availableToScroll =
400 getChildAt(0).getRight() - getScrollX() - rightEdge;
401 if (availableToScroll > 0)
403 deltaX = Math.min(availableToScroll, deltaX);
412 if (getScrollY() < 0)
419 final int bottomEdge = getHeight() - getPaddingBottom();
420 final int availableToScroll =
421 getChildAt(0).getBottom() - getScrollY() - bottomEdge;
422 if (availableToScroll > 0)
424 deltaY = Math.min(availableToScroll, deltaY);
431 if (deltaY != 0 || deltaX != 0)
432 scrollBy(deltaX, deltaY);
434 case MotionEvent.ACTION_UP:
435 final VelocityTracker velocityTracker = mVelocityTracker;
436 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
437 int initialXVelocity = (int)velocityTracker.getXVelocity();
438 int initialYVelocity = (int)velocityTracker.getYVelocity();
439 if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) &&
442 fling(-initialXVelocity, -initialYVelocity);
444 if (mVelocityTracker !=
null)
446 mVelocityTracker.recycle();
447 mVelocityTracker =
null;
468 private View findFocusableViewInMyBounds(
final boolean topFocus,
final int top,
469 final boolean leftFocus,
final int left,
470 View preferredFocusable)
477 final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2;
478 final int topWithoutFadingEdge = top + verticalFadingEdgeLength;
479 final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength;
480 final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
481 final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength;
482 final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength;
484 if ((preferredFocusable !=
null) &&
485 (preferredFocusable.getTop() < bottomWithoutFadingEdge) &&
486 (preferredFocusable.getBottom() > topWithoutFadingEdge) &&
487 (preferredFocusable.getLeft() < rightWithoutFadingEdge) &&
488 (preferredFocusable.getRight() > leftWithoutFadingEdge))
490 return preferredFocusable;
492 return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge,
493 leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
510 private View findFocusableViewInBounds(
boolean topFocus,
int top,
int bottom,
boolean leftFocus,
513 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
514 View focusCandidate =
null;
523 boolean foundFullyContainedFocusable =
false;
525 int count = focusables.size();
526 for (
int i = 0; i < count; i++)
528 View view = focusables.get(i);
529 int viewTop = view.getTop();
530 int viewBottom = view.getBottom();
531 int viewLeft = view.getLeft();
532 int viewRight = view.getRight();
534 if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right)
540 final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) &&
541 (left < viewLeft) && (viewRight < right);
542 if (focusCandidate ==
null)
545 focusCandidate = view;
546 foundFullyContainedFocusable = viewIsFullyContained;
550 final boolean viewIsCloserToVerticalBoundary =
551 (topFocus && viewTop < focusCandidate.getTop()) ||
552 (!topFocus && viewBottom > focusCandidate.getBottom());
553 final boolean viewIsCloserToHorizontalBoundary =
554 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
555 (!leftFocus && viewRight > focusCandidate.getRight());
556 if (foundFullyContainedFocusable)
558 if (viewIsFullyContained && viewIsCloserToVerticalBoundary &&
559 viewIsCloserToHorizontalBoundary)
566 focusCandidate = view;
571 if (viewIsFullyContained)
574 focusCandidate = view;
575 foundFullyContainedFocusable =
true;
577 else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary)
583 focusCandidate = view;
589 return focusCandidate;
608 boolean down = direction == View.FOCUS_DOWN;
609 int height = getHeight();
611 mTempRect.bottom = height;
614 int count = getChildCount();
617 View view = getChildAt(count - 1);
618 mTempRect.bottom = view.getBottom();
619 mTempRect.top = mTempRect.bottom - height;
622 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0);
626 boolean right = direction == View.FOCUS_DOWN;
627 int width = getWidth();
629 mTempRect.right = width;
632 int count = getChildCount();
635 View view = getChildAt(count - 1);
636 mTempRect.right = view.getBottom();
637 mTempRect.left = mTempRect.right - width;
640 return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom);
657 private boolean scrollAndFocus(
int directionY,
int top,
int bottom,
int directionX,
int left,
660 boolean handled =
true;
661 int height = getHeight();
662 int containerTop = getScrollY();
663 int containerBottom = containerTop + height;
664 boolean up = directionY == View.FOCUS_UP;
665 int width = getWidth();
666 int containerLeft = getScrollX();
667 int containerRight = containerLeft + width;
668 boolean leftwards = directionX == View.FOCUS_UP;
669 View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right);
670 if (newFocused ==
null)
674 if ((top >= containerTop && bottom <= containerBottom) ||
675 (left >= containerLeft && right <= containerRight))
681 int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
682 int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
683 doScroll(deltaX, deltaY);
685 if (newFocused != findFocus() && newFocused.requestFocus(directionY))
687 mTwoDScrollViewMovedFocus =
true;
688 mTwoDScrollViewMovedFocus =
false;
702 View currentFocused = findFocus();
703 if (currentFocused ==
this)
704 currentFocused =
null;
705 View nextFocused = FocusFinder.getInstance().findNextFocus(
this, currentFocused, direction);
711 if (nextFocused !=
null)
713 nextFocused.getDrawingRect(mTempRect);
714 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
716 doScroll(0, scrollDelta);
717 nextFocused.requestFocus(direction);
722 int scrollDelta = maxJump;
723 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
725 scrollDelta = getScrollY();
727 else if (direction == View.FOCUS_DOWN)
729 if (getChildCount() > 0)
731 int daBottom = getChildAt(0).getBottom();
732 int screenBottom = getScrollY() + getHeight();
733 if (daBottom - screenBottom < maxJump)
735 scrollDelta = daBottom - screenBottom;
739 if (scrollDelta == 0)
743 doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
748 if (nextFocused !=
null)
750 nextFocused.getDrawingRect(mTempRect);
751 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
753 doScroll(scrollDelta, 0);
754 nextFocused.requestFocus(direction);
759 int scrollDelta = maxJump;
760 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
762 scrollDelta = getScrollY();
764 else if (direction == View.FOCUS_DOWN)
766 if (getChildCount() > 0)
768 int daBottom = getChildAt(0).getBottom();
769 int screenBottom = getScrollY() + getHeight();
770 if (daBottom - screenBottom < maxJump)
772 scrollDelta = daBottom - screenBottom;
776 if (scrollDelta == 0)
780 doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
791 private void doScroll(
int deltaX,
int deltaY)
793 if (deltaX != 0 || deltaY != 0)
807 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
808 if (duration > ANIMATED_SCROLL_GAP)
810 mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
811 awakenScrollBars(mScroller.getDuration());
816 if (!mScroller.isFinished())
818 mScroller.abortAnimation();
822 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
842 int count = getChildCount();
843 return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
846 @Override
protected int computeHorizontalScrollRange()
848 int count = getChildCount();
849 return count == 0 ? getWidth() : (getChildAt(0)).getRight();
853 protected void measureChild(View child,
int parentWidthMeasureSpec,
int parentHeightMeasureSpec)
855 ViewGroup.LayoutParams lp = child.getLayoutParams();
856 int childWidthMeasureSpec;
857 int childHeightMeasureSpec;
859 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
860 getPaddingLeft() + getPaddingRight(), lp.width);
861 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
863 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
867 protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec,
int widthUsed,
868 int parentHeightMeasureSpec,
int heightUsed)
870 final MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
871 final int childWidthMeasureSpec =
872 MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
873 final int childHeightMeasureSpec =
874 MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
876 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
879 @Override
public void computeScroll()
881 if (mScroller.computeScrollOffset())
899 int oldX = getScrollX();
900 int oldY = getScrollY();
901 int x = mScroller.getCurrX();
902 int y = mScroller.getCurrY();
903 if (getChildCount() > 0)
905 View child = getChildAt(0);
907 clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()),
908 clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(),
915 if (oldX != getScrollX() || oldY != getScrollY())
917 onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
930 private void scrollToChild(View child)
932 child.getDrawingRect(mTempRect);
934 offsetDescendantRectToMyCoords(child, mTempRect);
936 if (scrollDelta != 0)
938 scrollBy(0, scrollDelta);
950 private boolean scrollToChildRect(Rect rect,
boolean immediate)
953 final boolean scroll = delta != 0;
978 if (getChildCount() == 0)
980 int height = getHeight();
981 int screenTop = getScrollY();
982 int screenBottom = screenTop + height;
983 int fadingEdge = getVerticalFadingEdgeLength();
987 screenTop += fadingEdge;
991 if (rect.bottom < getChildAt(0).getHeight())
993 screenBottom -= fadingEdge;
995 int scrollYDelta = 0;
996 if (rect.bottom > screenBottom && rect.top > screenTop)
1001 if (rect.height() > height)
1004 scrollYDelta += (rect.top - screenTop);
1009 scrollYDelta += (rect.bottom - screenBottom);
1013 int bottom = getChildAt(0).getBottom();
1014 int distanceToBottom = bottom - screenBottom;
1015 scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
1017 else if (rect.top < screenTop && rect.bottom < screenBottom)
1023 if (rect.height() > height)
1026 scrollYDelta -= (screenBottom - rect.bottom);
1031 scrollYDelta -= (screenTop - rect.top);
1035 scrollYDelta = Math.max(scrollYDelta, -getScrollY());
1037 return scrollYDelta;
1040 @Override
public void requestChildFocus(View child, View focused)
1042 if (!mTwoDScrollViewMovedFocus)
1044 if (!mIsLayoutDirty)
1046 scrollToChild(focused);
1051 mChildToScrollTo = focused;
1054 super.requestChildFocus(child, focused);
1069 if (direction == View.FOCUS_FORWARD)
1071 direction = View.FOCUS_DOWN;
1073 else if (direction == View.FOCUS_BACKWARD)
1075 direction = View.FOCUS_UP;
1078 final View nextFocus = previouslyFocusedRect ==
null
1079 ? FocusFinder.getInstance().findNextFocus(
this,
null, direction)
1080 : FocusFinder.getInstance().findNextFocusFromRect(
1081 this, previouslyFocusedRect, direction);
1083 if (nextFocus ==
null)
1088 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1092 public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
boolean immediate)
1095 rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
1096 return scrollToChildRect(rectangle, immediate);
1099 @Override
public void requestLayout()
1101 mIsLayoutDirty =
true;
1102 super.requestLayout();
1105 private int lastCenterContentH = -1;
1106 private int lastCenterTop = 0;
1108 @Override
protected void onLayout(
boolean changed,
int l,
int t,
int r,
int b)
1110 super.onLayout(changed, l, t, r, b);
1111 mIsLayoutDirty =
false;
1113 if (mChildToScrollTo !=
null && isViewDescendantOf(mChildToScrollTo,
this))
1115 scrollToChild(mChildToScrollTo);
1117 mChildToScrollTo =
null;
1121 if (getChildCount() > 0)
1123 View child = getChildAt(0);
1124 SessionView sv = findViewById(R.id.sessionView);
1125 int ptw = sv !=
null ? sv.getTouchPointerPaddingWidth() : 0;
1126 int pth = sv !=
null ? sv.getTouchPointerPaddingHeight() : 0;
1127 int contentW = child.getMeasuredWidth() - ptw;
1128 int contentH = child.getMeasuredHeight() - pth;
1129 int usableW = getWidth() - getPaddingLeft() - getPaddingRight();
1130 int usableH = getHeight() - getPaddingTop() - getPaddingBottom();
1131 int left = getPaddingLeft() + Math.max(0, (usableW - contentW) / 2);
1133 if (contentH == lastCenterContentH)
1136 top = lastCenterTop;
1140 top = getPaddingTop() + Math.max(0, (usableH - contentH) / 2);
1141 lastCenterContentH = contentH;
1142 lastCenterTop = top;
1144 child.layout(left, top, left + child.getMeasuredWidth(),
1145 top + child.getMeasuredHeight());
1149 scrollTo(getScrollX(), getScrollY());
1152 @Override
protected void onSizeChanged(
int w,
int h,
int oldw,
int oldh)
1154 super.onSizeChanged(w, h, oldw, oldh);
1156 View currentFocused = findFocus();
1157 if (
null == currentFocused ||
this == currentFocused)
1163 currentFocused.getDrawingRect(mTempRect);
1164 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1167 doScroll(scrollDeltaX, scrollDeltaY);
1173 private boolean isViewDescendantOf(View child, View parent)
1175 if (child == parent)
1180 final ViewParent theParent = child.getParent();
1181 return (theParent instanceof ViewGroup) && isViewDescendantOf((View)theParent, parent);
1191 public void fling(
int velocityX,
int velocityY)
1193 if (getChildCount() > 0)
1195 int height = getHeight() - getPaddingBottom() - getPaddingTop();
1196 int bottom = getChildAt(0).getHeight();
1197 int width = getWidth() - getPaddingRight() - getPaddingLeft();
1198 int right = getChildAt(0).getWidth();
1200 mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0,
1203 final boolean movingDown = velocityY > 0;
1204 final boolean movingRight = velocityX > 0;
1206 View newFocused = findFocusableViewInMyBounds(
1207 movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus());
1208 if (newFocused ==
null)
1213 if (newFocused != findFocus() &&
1214 newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP))
1216 mTwoDScrollViewMovedFocus =
true;
1217 mTwoDScrollViewMovedFocus =
false;
1220 awakenScrollBars(mScroller.getDuration());
1233 if (getChildCount() > 0)
1235 View child = getChildAt(0);
1236 x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
1237 y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
1238 if (x != getScrollX() || y != getScrollY())
1240 super.scrollTo(x, y);
1245 private int clamp(
int n,
int my,
int child)
1247 if (my >= child || n < 0)
1266 if ((my + n) > child)
1278 public void setScrollViewListener(ScrollView2DListener scrollViewListener)
1280 this.scrollView2DListener = scrollViewListener;
1283 @Override
protected void onScrollChanged(
int x,
int y,
int oldx,
int oldy)
1285 super.onScrollChanged(x, y, oldx, oldy);
1286 if (scrollView2DListener !=
null)
1288 scrollView2DListener.onScrollChanged(
this, x, y, oldx, oldy);
1294 void onScrollChanged(
ScrollView2D scrollView,
int x,
int y,
int oldx,
int oldy);