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)
284 if (ev.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE)
286 mIsBeingDragged =
false;
290 final int action = ev.getAction();
291 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged))
297 mIsBeingDragged =
false;
300 final float y = ev.getY();
301 final float x = ev.getX();
304 case MotionEvent.ACTION_MOVE:
313 final int yDiff = (int)Math.abs(y - mLastMotionY);
314 final int xDiff = (int)Math.abs(x - mLastMotionX);
315 if (yDiff > mTouchSlop || xDiff > mTouchSlop)
317 mIsBeingDragged =
true;
321 case MotionEvent.ACTION_DOWN:
331 mIsBeingDragged = !mScroller.isFinished();
334 case MotionEvent.ACTION_CANCEL:
335 case MotionEvent.ACTION_UP:
337 mIsBeingDragged =
false;
345 return mIsBeingDragged;
348 @Override
public boolean onTouchEvent(MotionEvent ev)
351 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0)
363 if (mVelocityTracker ==
null)
365 mVelocityTracker = VelocityTracker.obtain();
367 mVelocityTracker.addMovement(ev);
369 final int action = ev.getAction();
370 final float y = ev.getY();
371 final float x = ev.getX();
375 case MotionEvent.ACTION_DOWN:
380 if (!mScroller.isFinished())
382 mScroller.abortAnimation();
389 case MotionEvent.ACTION_MOVE:
391 int deltaX = (int)(mLastMotionX - x);
392 int deltaY = (int)(mLastMotionY - y);
398 if (getScrollX() < 0)
405 final int rightEdge = getWidth() - getPaddingRight();
406 final int availableToScroll =
407 getChildAt(0).getRight() - getScrollX() - rightEdge;
408 if (availableToScroll > 0)
410 deltaX = Math.min(availableToScroll, deltaX);
419 if (getScrollY() < 0)
426 final int bottomEdge = getHeight() - getPaddingBottom();
427 final int availableToScroll =
428 getChildAt(0).getBottom() - getScrollY() - bottomEdge;
429 if (availableToScroll > 0)
431 deltaY = Math.min(availableToScroll, deltaY);
438 if (deltaY != 0 || deltaX != 0)
439 scrollBy(deltaX, deltaY);
441 case MotionEvent.ACTION_UP:
442 final VelocityTracker velocityTracker = mVelocityTracker;
443 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
444 int initialXVelocity = (int)velocityTracker.getXVelocity();
445 int initialYVelocity = (int)velocityTracker.getYVelocity();
446 if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) &&
449 fling(-initialXVelocity, -initialYVelocity);
451 if (mVelocityTracker !=
null)
453 mVelocityTracker.recycle();
454 mVelocityTracker =
null;
475 private View findFocusableViewInMyBounds(
final boolean topFocus,
final int top,
476 final boolean leftFocus,
final int left,
477 View preferredFocusable)
484 final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2;
485 final int topWithoutFadingEdge = top + verticalFadingEdgeLength;
486 final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength;
487 final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
488 final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength;
489 final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength;
491 if ((preferredFocusable !=
null) &&
492 (preferredFocusable.getTop() < bottomWithoutFadingEdge) &&
493 (preferredFocusable.getBottom() > topWithoutFadingEdge) &&
494 (preferredFocusable.getLeft() < rightWithoutFadingEdge) &&
495 (preferredFocusable.getRight() > leftWithoutFadingEdge))
497 return preferredFocusable;
499 return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge,
500 leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
517 private View findFocusableViewInBounds(
boolean topFocus,
int top,
int bottom,
boolean leftFocus,
520 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
521 View focusCandidate =
null;
530 boolean foundFullyContainedFocusable =
false;
532 int count = focusables.size();
533 for (
int i = 0; i < count; i++)
535 View view = focusables.get(i);
536 int viewTop = view.getTop();
537 int viewBottom = view.getBottom();
538 int viewLeft = view.getLeft();
539 int viewRight = view.getRight();
541 if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right)
547 final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) &&
548 (left < viewLeft) && (viewRight < right);
549 if (focusCandidate ==
null)
552 focusCandidate = view;
553 foundFullyContainedFocusable = viewIsFullyContained;
557 final boolean viewIsCloserToVerticalBoundary =
558 (topFocus && viewTop < focusCandidate.getTop()) ||
559 (!topFocus && viewBottom > focusCandidate.getBottom());
560 final boolean viewIsCloserToHorizontalBoundary =
561 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
562 (!leftFocus && viewRight > focusCandidate.getRight());
563 if (foundFullyContainedFocusable)
565 if (viewIsFullyContained && viewIsCloserToVerticalBoundary &&
566 viewIsCloserToHorizontalBoundary)
573 focusCandidate = view;
578 if (viewIsFullyContained)
581 focusCandidate = view;
582 foundFullyContainedFocusable =
true;
584 else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary)
590 focusCandidate = view;
596 return focusCandidate;
615 boolean down = direction == View.FOCUS_DOWN;
616 int height = getHeight();
618 mTempRect.bottom = height;
621 int count = getChildCount();
624 View view = getChildAt(count - 1);
625 mTempRect.bottom = view.getBottom();
626 mTempRect.top = mTempRect.bottom - height;
629 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0);
633 boolean right = direction == View.FOCUS_DOWN;
634 int width = getWidth();
636 mTempRect.right = width;
639 int count = getChildCount();
642 View view = getChildAt(count - 1);
643 mTempRect.right = view.getBottom();
644 mTempRect.left = mTempRect.right - width;
647 return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom);
664 private boolean scrollAndFocus(
int directionY,
int top,
int bottom,
int directionX,
int left,
667 boolean handled =
true;
668 int height = getHeight();
669 int containerTop = getScrollY();
670 int containerBottom = containerTop + height;
671 boolean up = directionY == View.FOCUS_UP;
672 int width = getWidth();
673 int containerLeft = getScrollX();
674 int containerRight = containerLeft + width;
675 boolean leftwards = directionX == View.FOCUS_UP;
676 View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right);
677 if (newFocused ==
null)
681 if ((top >= containerTop && bottom <= containerBottom) ||
682 (left >= containerLeft && right <= containerRight))
688 int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
689 int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
690 doScroll(deltaX, deltaY);
692 if (newFocused != findFocus() && newFocused.requestFocus(directionY))
694 mTwoDScrollViewMovedFocus =
true;
695 mTwoDScrollViewMovedFocus =
false;
709 View currentFocused = findFocus();
710 if (currentFocused ==
this)
711 currentFocused =
null;
712 View nextFocused = FocusFinder.getInstance().findNextFocus(
this, currentFocused, direction);
718 if (nextFocused !=
null)
720 nextFocused.getDrawingRect(mTempRect);
721 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
723 doScroll(0, scrollDelta);
724 nextFocused.requestFocus(direction);
729 int scrollDelta = maxJump;
730 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
732 scrollDelta = getScrollY();
734 else if (direction == View.FOCUS_DOWN)
736 if (getChildCount() > 0)
738 int daBottom = getChildAt(0).getBottom();
739 int screenBottom = getScrollY() + getHeight();
740 if (daBottom - screenBottom < maxJump)
742 scrollDelta = daBottom - screenBottom;
746 if (scrollDelta == 0)
750 doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
755 if (nextFocused !=
null)
757 nextFocused.getDrawingRect(mTempRect);
758 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
760 doScroll(scrollDelta, 0);
761 nextFocused.requestFocus(direction);
766 int scrollDelta = maxJump;
767 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
769 scrollDelta = getScrollY();
771 else if (direction == View.FOCUS_DOWN)
773 if (getChildCount() > 0)
775 int daBottom = getChildAt(0).getBottom();
776 int screenBottom = getScrollY() + getHeight();
777 if (daBottom - screenBottom < maxJump)
779 scrollDelta = daBottom - screenBottom;
783 if (scrollDelta == 0)
787 doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
798 private void doScroll(
int deltaX,
int deltaY)
800 if (deltaX != 0 || deltaY != 0)
814 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
815 if (duration > ANIMATED_SCROLL_GAP)
817 mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
818 awakenScrollBars(mScroller.getDuration());
823 if (!mScroller.isFinished())
825 mScroller.abortAnimation();
829 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
849 int count = getChildCount();
850 return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
853 @Override
protected int computeHorizontalScrollRange()
855 int count = getChildCount();
856 return count == 0 ? getWidth() : (getChildAt(0)).getRight();
860 protected void measureChild(View child,
int parentWidthMeasureSpec,
int parentHeightMeasureSpec)
862 ViewGroup.LayoutParams lp = child.getLayoutParams();
863 int childWidthMeasureSpec;
864 int childHeightMeasureSpec;
866 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
867 getPaddingLeft() + getPaddingRight(), lp.width);
868 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
870 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
874 protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec,
int widthUsed,
875 int parentHeightMeasureSpec,
int heightUsed)
877 final MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
878 final int childWidthMeasureSpec =
879 MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
880 final int childHeightMeasureSpec =
881 MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
883 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
886 @Override
public void computeScroll()
888 if (mScroller.computeScrollOffset())
906 int oldX = getScrollX();
907 int oldY = getScrollY();
908 int x = mScroller.getCurrX();
909 int y = mScroller.getCurrY();
910 if (getChildCount() > 0)
912 View child = getChildAt(0);
914 clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()),
915 clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(),
922 if (oldX != getScrollX() || oldY != getScrollY())
924 onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
937 private void scrollToChild(View child)
939 child.getDrawingRect(mTempRect);
941 offsetDescendantRectToMyCoords(child, mTempRect);
943 if (scrollDelta != 0)
945 scrollBy(0, scrollDelta);
957 private boolean scrollToChildRect(Rect rect,
boolean immediate)
960 final boolean scroll = delta != 0;
985 if (getChildCount() == 0)
987 int height = getHeight();
988 int screenTop = getScrollY();
989 int screenBottom = screenTop + height;
990 int fadingEdge = getVerticalFadingEdgeLength();
994 screenTop += fadingEdge;
998 if (rect.bottom < getChildAt(0).getHeight())
1000 screenBottom -= fadingEdge;
1002 int scrollYDelta = 0;
1003 if (rect.bottom > screenBottom && rect.top > screenTop)
1008 if (rect.height() > height)
1011 scrollYDelta += (rect.top - screenTop);
1016 scrollYDelta += (rect.bottom - screenBottom);
1020 int bottom = getChildAt(0).getBottom();
1021 int distanceToBottom = bottom - screenBottom;
1022 scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
1024 else if (rect.top < screenTop && rect.bottom < screenBottom)
1030 if (rect.height() > height)
1033 scrollYDelta -= (screenBottom - rect.bottom);
1038 scrollYDelta -= (screenTop - rect.top);
1042 scrollYDelta = Math.max(scrollYDelta, -getScrollY());
1044 return scrollYDelta;
1047 @Override
public void requestChildFocus(View child, View focused)
1049 if (!mTwoDScrollViewMovedFocus)
1051 if (!mIsLayoutDirty)
1053 scrollToChild(focused);
1058 mChildToScrollTo = focused;
1061 super.requestChildFocus(child, focused);
1076 if (direction == View.FOCUS_FORWARD)
1078 direction = View.FOCUS_DOWN;
1080 else if (direction == View.FOCUS_BACKWARD)
1082 direction = View.FOCUS_UP;
1085 final View nextFocus = previouslyFocusedRect ==
null
1086 ? FocusFinder.getInstance().findNextFocus(
this,
null, direction)
1087 : FocusFinder.getInstance().findNextFocusFromRect(
1088 this, previouslyFocusedRect, direction);
1090 if (nextFocus ==
null)
1095 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1099 public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
boolean immediate)
1102 rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
1103 return scrollToChildRect(rectangle, immediate);
1106 @Override
public void requestLayout()
1108 mIsLayoutDirty =
true;
1109 super.requestLayout();
1112 private int lastCenterContentH = -1;
1113 private int lastCenterTop = 0;
1115 @Override
protected void onLayout(
boolean changed,
int l,
int t,
int r,
int b)
1117 super.onLayout(changed, l, t, r, b);
1118 mIsLayoutDirty =
false;
1120 if (mChildToScrollTo !=
null && isViewDescendantOf(mChildToScrollTo,
this))
1122 scrollToChild(mChildToScrollTo);
1124 mChildToScrollTo =
null;
1128 if (getChildCount() > 0)
1130 View child = getChildAt(0);
1131 SessionView sv = findViewById(R.id.sessionView);
1132 int ptw = sv !=
null ? sv.getTouchPointerPaddingWidth() : 0;
1133 int pth = sv !=
null ? sv.getTouchPointerPaddingHeight() : 0;
1134 int contentW = child.getMeasuredWidth() - ptw;
1135 int contentH = child.getMeasuredHeight() - pth;
1136 int usableW = getWidth() - getPaddingLeft() - getPaddingRight();
1137 int usableH = getHeight() - getPaddingTop() - getPaddingBottom();
1138 int left = getPaddingLeft() + Math.max(0, (usableW - contentW) / 2);
1140 if (contentH == lastCenterContentH)
1143 top = lastCenterTop;
1147 top = getPaddingTop() + Math.max(0, (usableH - contentH) / 2);
1148 lastCenterContentH = contentH;
1149 lastCenterTop = top;
1151 child.layout(left, top, left + child.getMeasuredWidth(),
1152 top + child.getMeasuredHeight());
1156 scrollTo(getScrollX(), getScrollY());
1159 @Override
protected void onSizeChanged(
int w,
int h,
int oldw,
int oldh)
1161 super.onSizeChanged(w, h, oldw, oldh);
1163 View currentFocused = findFocus();
1164 if (
null == currentFocused ||
this == currentFocused)
1170 currentFocused.getDrawingRect(mTempRect);
1171 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1174 doScroll(scrollDeltaX, scrollDeltaY);
1180 private boolean isViewDescendantOf(View child, View parent)
1182 if (child == parent)
1187 final ViewParent theParent = child.getParent();
1188 return (theParent instanceof ViewGroup) && isViewDescendantOf((View)theParent, parent);
1198 public void fling(
int velocityX,
int velocityY)
1200 if (getChildCount() > 0)
1202 int height = getHeight() - getPaddingBottom() - getPaddingTop();
1203 int bottom = getChildAt(0).getHeight();
1204 int width = getWidth() - getPaddingRight() - getPaddingLeft();
1205 int right = getChildAt(0).getWidth();
1207 mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0,
1210 final boolean movingDown = velocityY > 0;
1211 final boolean movingRight = velocityX > 0;
1213 View newFocused = findFocusableViewInMyBounds(
1214 movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus());
1215 if (newFocused ==
null)
1220 if (newFocused != findFocus() &&
1221 newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP))
1223 mTwoDScrollViewMovedFocus =
true;
1224 mTwoDScrollViewMovedFocus =
false;
1227 awakenScrollBars(mScroller.getDuration());
1240 if (getChildCount() > 0)
1242 View child = getChildAt(0);
1243 x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
1244 y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
1245 if (x != getScrollX() || y != getScrollY())
1247 super.scrollTo(x, y);
1252 private int clamp(
int n,
int my,
int child)
1254 if (my >= child || n < 0)
1273 if ((my + n) > child)
1285 public void setScrollViewListener(ScrollView2DListener scrollViewListener)
1287 this.scrollView2DListener = scrollViewListener;
1290 @Override
protected void onScrollChanged(
int x,
int y,
int oldx,
int oldy)
1292 super.onScrollChanged(x, y, oldx, oldy);
1293 if (scrollView2DListener !=
null)
1295 scrollView2DListener.onScrollChanged(
this, x, y, oldx, oldy);
1301 void onScrollChanged(
ScrollView2D scrollView,
int x,
int y,
int oldx,
int oldy);