FreeRDP
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Modules Pages
ScrollView2D.java
1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16/*
17 * Revised 5/19/2010 by GORGES
18 * Now supports two-dimensional view scrolling
19 * http://GORGES.us
20 */
21
22package com.freerdp.freerdpcore.presentation;
23
24import android.content.Context;
25import android.graphics.Rect;
26import android.util.AttributeSet;
27import android.view.FocusFinder;
28import android.view.KeyEvent;
29import android.view.MotionEvent;
30import android.view.VelocityTracker;
31import android.view.View;
32import android.view.ViewConfiguration;
33import android.view.ViewGroup;
34import android.view.ViewParent;
35import android.view.animation.AnimationUtils;
36import android.widget.FrameLayout;
37import android.widget.LinearLayout;
38import android.widget.Scroller;
39import android.widget.TextView;
40
41import java.util.List;
42
57public class ScrollView2D extends FrameLayout
58{
59
60 static final int ANIMATED_SCROLL_GAP = 250;
61 static final float MAX_SCROLL_FACTOR = 0.5f;
62 private final Rect mTempRect = new Rect();
63 private ScrollView2DListener scrollView2DListener = null;
64 private long mLastScroll;
65 private Scroller mScroller;
66 private boolean scrollEnabled = true;
72 private boolean mTwoDScrollViewMovedFocus;
76 private float mLastMotionY;
77 private float mLastMotionX;
82 private boolean mIsLayoutDirty = true;
88 private View mChildToScrollTo = null;
94 private boolean mIsBeingDragged = false;
98 private VelocityTracker mVelocityTracker;
102 private int mTouchSlop;
103 private int mMinimumVelocity;
104 private int mMaximumVelocity;
105 public ScrollView2D(Context context)
106 {
107 super(context);
108 initTwoDScrollView();
109 }
110
111 public ScrollView2D(Context context, AttributeSet attrs)
112 {
113 super(context, attrs);
114 initTwoDScrollView();
115 }
116
117 public ScrollView2D(Context context, AttributeSet attrs, int defStyle)
118 {
119 super(context, attrs, defStyle);
120 initTwoDScrollView();
121 }
122
123 @Override protected float getTopFadingEdgeStrength()
124 {
125 if (getChildCount() == 0)
126 {
127 return 0.0f;
128 }
129 final int length = getVerticalFadingEdgeLength();
130 if (getScrollY() < length)
131 {
132 return getScrollY() / (float)length;
133 }
134 return 1.0f;
135 }
136
137 @Override protected float getBottomFadingEdgeStrength()
138 {
139 if (getChildCount() == 0)
140 {
141 return 0.0f;
142 }
143 final int length = getVerticalFadingEdgeLength();
144 final int bottomEdge = getHeight() - getPaddingBottom();
145 final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
146 if (span < length)
147 {
148 return span / (float)length;
149 }
150 return 1.0f;
151 }
152
153 @Override protected float getLeftFadingEdgeStrength()
154 {
155 if (getChildCount() == 0)
156 {
157 return 0.0f;
158 }
159 final int length = getHorizontalFadingEdgeLength();
160 if (getScrollX() < length)
161 {
162 return getScrollX() / (float)length;
163 }
164 return 1.0f;
165 }
166
167 @Override protected float getRightFadingEdgeStrength()
168 {
169 if (getChildCount() == 0)
170 {
171 return 0.0f;
172 }
173 final int length = getHorizontalFadingEdgeLength();
174 final int rightEdge = getWidth() - getPaddingRight();
175 final int span = getChildAt(0).getRight() - getScrollX() - rightEdge;
176 if (span < length)
177 {
178 return span / (float)length;
179 }
180 return 1.0f;
181 }
182
186 public void setScrollEnabled(boolean enable)
187 {
188 scrollEnabled = enable;
189 }
190
196 {
197 return (int)(MAX_SCROLL_FACTOR * getHeight());
198 }
199
200 public int getMaxScrollAmountHorizontal()
201 {
202 return (int)(MAX_SCROLL_FACTOR * getWidth());
203 }
204
205 private void initTwoDScrollView()
206 {
207 mScroller = new Scroller(getContext());
208 setFocusable(true);
209 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
210 setWillNotDraw(false);
211 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
212 mTouchSlop = configuration.getScaledTouchSlop();
213 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
214 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
215 }
216
217 @Override public void addView(View child)
218 {
219 if (getChildCount() > 0)
220 {
221 throw new IllegalStateException("TwoDScrollView can host only one direct child");
222 }
223 super.addView(child);
224 }
225
226 @Override public void addView(View child, int index)
227 {
228 if (getChildCount() > 0)
229 {
230 throw new IllegalStateException("TwoDScrollView can host only one direct child");
231 }
232 super.addView(child, index);
233 }
234
235 @Override public void addView(View child, ViewGroup.LayoutParams params)
236 {
237 if (getChildCount() > 0)
238 {
239 throw new IllegalStateException("TwoDScrollView can host only one direct child");
240 }
241 super.addView(child, params);
242 }
243
244 @Override public void addView(View child, int index, ViewGroup.LayoutParams params)
245 {
246 if (getChildCount() > 0)
247 {
248 throw new IllegalStateException("TwoDScrollView can host only one direct child");
249 }
250 super.addView(child, index, params);
251 }
252
256 private boolean canScroll()
257 {
258 if (!scrollEnabled)
259 return false;
260 View child = getChildAt(0);
261 if (child != null)
262 {
263 int childHeight = child.getHeight();
264 int childWidth = child.getWidth();
265 return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) ||
266 (getWidth() < childWidth + getPaddingLeft() + getPaddingRight());
267 }
268 return false;
269 }
270
271 @Override public boolean dispatchKeyEvent(KeyEvent event)
272 {
273 // Let the focused view and/or our descendants get the key first
274 boolean handled = super.dispatchKeyEvent(event);
275 if (handled)
276 {
277 return true;
278 }
279 return executeKeyEvent(event);
280 }
281
290 public boolean executeKeyEvent(KeyEvent event)
291 {
292 mTempRect.setEmpty();
293 if (!canScroll())
294 {
295 if (isFocused())
296 {
297 View currentFocused = findFocus();
298 if (currentFocused == this)
299 currentFocused = null;
300 View nextFocused =
301 FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN);
302 return nextFocused != null && nextFocused != this &&
303 nextFocused.requestFocus(View.FOCUS_DOWN);
304 }
305 return false;
306 }
307 boolean handled = false;
308 if (event.getAction() == KeyEvent.ACTION_DOWN)
309 {
310 switch (event.getKeyCode())
311 {
312 case KeyEvent.KEYCODE_DPAD_UP:
313 if (!event.isAltPressed())
314 {
315 handled = arrowScroll(View.FOCUS_UP, false);
316 }
317 else
318 {
319 handled = fullScroll(View.FOCUS_UP, false);
320 }
321 break;
322 case KeyEvent.KEYCODE_DPAD_DOWN:
323 if (!event.isAltPressed())
324 {
325 handled = arrowScroll(View.FOCUS_DOWN, false);
326 }
327 else
328 {
329 handled = fullScroll(View.FOCUS_DOWN, false);
330 }
331 break;
332 case KeyEvent.KEYCODE_DPAD_LEFT:
333 if (!event.isAltPressed())
334 {
335 handled = arrowScroll(View.FOCUS_LEFT, true);
336 }
337 else
338 {
339 handled = fullScroll(View.FOCUS_LEFT, true);
340 }
341 break;
342 case KeyEvent.KEYCODE_DPAD_RIGHT:
343 if (!event.isAltPressed())
344 {
345 handled = arrowScroll(View.FOCUS_RIGHT, true);
346 }
347 else
348 {
349 handled = fullScroll(View.FOCUS_RIGHT, true);
350 }
351 break;
352 }
353 }
354 return handled;
355 }
356
357 @Override public boolean onInterceptTouchEvent(MotionEvent ev)
358 {
359 /*
360 * This method JUST determines whether we want to intercept the motion.
361 * If we return true, onMotionEvent will be called and we do the actual
362 * scrolling there.
363 *
364 * Shortcut the most recurring case: the user is in the dragging
365 * state and he is moving his finger. We want to intercept this
366 * motion.
367 */
368 final int action = ev.getAction();
369 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged))
370 {
371 return true;
372 }
373 if (!canScroll())
374 {
375 mIsBeingDragged = false;
376 return false;
377 }
378 final float y = ev.getY();
379 final float x = ev.getX();
380 switch (action)
381 {
382 case MotionEvent.ACTION_MOVE:
383 /*
384 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
385 * whether the user has moved far enough from his original down touch.
386 */
387 /*
388 * Locally do absolute value. mLastMotionY is set to the y value
389 * of the down event.
390 */
391 final int yDiff = (int)Math.abs(y - mLastMotionY);
392 final int xDiff = (int)Math.abs(x - mLastMotionX);
393 if (yDiff > mTouchSlop || xDiff > mTouchSlop)
394 {
395 mIsBeingDragged = true;
396 }
397 break;
398
399 case MotionEvent.ACTION_DOWN:
400 /* Remember location of down touch */
401 mLastMotionY = y;
402 mLastMotionX = x;
403
404 /*
405 * If being flinged and user touches the screen, initiate drag;
406 * otherwise don't. mScroller.isFinished should be false when
407 * being flinged.
408 */
409 mIsBeingDragged = !mScroller.isFinished();
410 break;
411
412 case MotionEvent.ACTION_CANCEL:
413 case MotionEvent.ACTION_UP:
414 /* Release the drag */
415 mIsBeingDragged = false;
416 break;
417 }
418
419 /*
420 * The only time we want to intercept motion events is if we are in the
421 * drag mode.
422 */
423 return mIsBeingDragged;
424 }
425
426 @Override public boolean onTouchEvent(MotionEvent ev)
427 {
428
429 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0)
430 {
431 // Don't handle edge touches immediately -- they may actually belong to one of our
432 // descendants.
433 return false;
434 }
435
436 if (!canScroll())
437 {
438 return false;
439 }
440
441 if (mVelocityTracker == null)
442 {
443 mVelocityTracker = VelocityTracker.obtain();
444 }
445 mVelocityTracker.addMovement(ev);
446
447 final int action = ev.getAction();
448 final float y = ev.getY();
449 final float x = ev.getX();
450
451 switch (action)
452 {
453 case MotionEvent.ACTION_DOWN:
454 /*
455 * If being flinged and user touches, stop the fling. isFinished
456 * will be false if being flinged.
457 */
458 if (!mScroller.isFinished())
459 {
460 mScroller.abortAnimation();
461 }
462
463 // Remember where the motion event started
464 mLastMotionY = y;
465 mLastMotionX = x;
466 break;
467 case MotionEvent.ACTION_MOVE:
468 // Scroll to follow the motion event
469 int deltaX = (int)(mLastMotionX - x);
470 int deltaY = (int)(mLastMotionY - y);
471 mLastMotionX = x;
472 mLastMotionY = y;
473
474 if (deltaX < 0)
475 {
476 if (getScrollX() < 0)
477 {
478 deltaX = 0;
479 }
480 }
481 else if (deltaX > 0)
482 {
483 final int rightEdge = getWidth() - getPaddingRight();
484 final int availableToScroll =
485 getChildAt(0).getRight() - getScrollX() - rightEdge;
486 if (availableToScroll > 0)
487 {
488 deltaX = Math.min(availableToScroll, deltaX);
489 }
490 else
491 {
492 deltaX = 0;
493 }
494 }
495 if (deltaY < 0)
496 {
497 if (getScrollY() < 0)
498 {
499 deltaY = 0;
500 }
501 }
502 else if (deltaY > 0)
503 {
504 final int bottomEdge = getHeight() - getPaddingBottom();
505 final int availableToScroll =
506 getChildAt(0).getBottom() - getScrollY() - bottomEdge;
507 if (availableToScroll > 0)
508 {
509 deltaY = Math.min(availableToScroll, deltaY);
510 }
511 else
512 {
513 deltaY = 0;
514 }
515 }
516 if (deltaY != 0 || deltaX != 0)
517 scrollBy(deltaX, deltaY);
518 break;
519 case MotionEvent.ACTION_UP:
520 final VelocityTracker velocityTracker = mVelocityTracker;
521 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
522 int initialXVelocity = (int)velocityTracker.getXVelocity();
523 int initialYVelocity = (int)velocityTracker.getYVelocity();
524 if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) &&
525 getChildCount() > 0)
526 {
527 fling(-initialXVelocity, -initialYVelocity);
528 }
529 if (mVelocityTracker != null)
530 {
531 mVelocityTracker.recycle();
532 mVelocityTracker = null;
533 }
534 }
535 return true;
536 }
537
553 private View findFocusableViewInMyBounds(final boolean topFocus, final int top,
554 final boolean leftFocus, final int left,
555 View preferredFocusable)
556 {
557 /*
558 * The fading edge's transparent side should be considered for focus
559 * since it's mostly visible, so we divide the actual fading edge length
560 * by 2.
561 */
562 final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2;
563 final int topWithoutFadingEdge = top + verticalFadingEdgeLength;
564 final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength;
565 final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
566 final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength;
567 final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength;
568
569 if ((preferredFocusable != null) &&
570 (preferredFocusable.getTop() < bottomWithoutFadingEdge) &&
571 (preferredFocusable.getBottom() > topWithoutFadingEdge) &&
572 (preferredFocusable.getLeft() < rightWithoutFadingEdge) &&
573 (preferredFocusable.getRight() > leftWithoutFadingEdge))
574 {
575 return preferredFocusable;
576 }
577 return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge,
578 leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
579 }
580
595 private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus,
596 int left, int right)
597 {
598 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
599 View focusCandidate = null;
600
601 /*
602 * A fully contained focusable is one where its top is below the bound's
603 * top, and its bottom is above the bound's bottom. A partially
604 * contained focusable is one where some part of it is within the
605 * bounds, but it also has some part that is not within bounds. A fully contained
606 * focusable is preferred to a partially contained focusable.
607 */
608 boolean foundFullyContainedFocusable = false;
609
610 int count = focusables.size();
611 for (int i = 0; i < count; i++)
612 {
613 View view = focusables.get(i);
614 int viewTop = view.getTop();
615 int viewBottom = view.getBottom();
616 int viewLeft = view.getLeft();
617 int viewRight = view.getRight();
618
619 if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right)
620 {
621 /*
622 * the focusable is in the target area, it is a candidate for
623 * focusing
624 */
625 final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) &&
626 (left < viewLeft) && (viewRight < right);
627 if (focusCandidate == null)
628 {
629 /* No candidate, take this one */
630 focusCandidate = view;
631 foundFullyContainedFocusable = viewIsFullyContained;
632 }
633 else
634 {
635 final boolean viewIsCloserToVerticalBoundary =
636 (topFocus && viewTop < focusCandidate.getTop()) ||
637 (!topFocus && viewBottom > focusCandidate.getBottom());
638 final boolean viewIsCloserToHorizontalBoundary =
639 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
640 (!leftFocus && viewRight > focusCandidate.getRight());
641 if (foundFullyContainedFocusable)
642 {
643 if (viewIsFullyContained && viewIsCloserToVerticalBoundary &&
644 viewIsCloserToHorizontalBoundary)
645 {
646 /*
647 * We're dealing with only fully contained views, so
648 * it has to be closer to the boundary to beat our
649 * candidate
650 */
651 focusCandidate = view;
652 }
653 }
654 else
655 {
656 if (viewIsFullyContained)
657 {
658 /* Any fully contained view beats a partially contained view */
659 focusCandidate = view;
660 foundFullyContainedFocusable = true;
661 }
662 else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary)
663 {
664 /*
665 * Partially contained view beats another partially
666 * contained view if it's closer
667 */
668 focusCandidate = view;
669 }
670 }
671 }
672 }
673 }
674 return focusCandidate;
675 }
676
689 public boolean fullScroll(int direction, boolean horizontal)
690 {
691 if (!horizontal)
692 {
693 boolean down = direction == View.FOCUS_DOWN;
694 int height = getHeight();
695 mTempRect.top = 0;
696 mTempRect.bottom = height;
697 if (down)
698 {
699 int count = getChildCount();
700 if (count > 0)
701 {
702 View view = getChildAt(count - 1);
703 mTempRect.bottom = view.getBottom();
704 mTempRect.top = mTempRect.bottom - height;
705 }
706 }
707 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0);
708 }
709 else
710 {
711 boolean right = direction == View.FOCUS_DOWN;
712 int width = getWidth();
713 mTempRect.left = 0;
714 mTempRect.right = width;
715 if (right)
716 {
717 int count = getChildCount();
718 if (count > 0)
719 {
720 View view = getChildAt(count - 1);
721 mTempRect.right = view.getBottom();
722 mTempRect.left = mTempRect.right - width;
723 }
724 }
725 return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom);
726 }
727 }
728
742 private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left,
743 int right)
744 {
745 boolean handled = true;
746 int height = getHeight();
747 int containerTop = getScrollY();
748 int containerBottom = containerTop + height;
749 boolean up = directionY == View.FOCUS_UP;
750 int width = getWidth();
751 int containerLeft = getScrollX();
752 int containerRight = containerLeft + width;
753 boolean leftwards = directionX == View.FOCUS_UP;
754 View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right);
755 if (newFocused == null)
756 {
757 newFocused = this;
758 }
759 if ((top >= containerTop && bottom <= containerBottom) ||
760 (left >= containerLeft && right <= containerRight))
761 {
762 handled = false;
763 }
764 else
765 {
766 int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
767 int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
768 doScroll(deltaX, deltaY);
769 }
770 if (newFocused != findFocus() && newFocused.requestFocus(directionY))
771 {
772 mTwoDScrollViewMovedFocus = true;
773 mTwoDScrollViewMovedFocus = false;
774 }
775 return handled;
776 }
777
785 public boolean arrowScroll(int direction, boolean horizontal)
786 {
787 View currentFocused = findFocus();
788 if (currentFocused == this)
789 currentFocused = null;
790 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
791 final int maxJump =
792 horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical();
793
794 if (!horizontal)
795 {
796 if (nextFocused != null)
797 {
798 nextFocused.getDrawingRect(mTempRect);
799 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
800 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
801 doScroll(0, scrollDelta);
802 nextFocused.requestFocus(direction);
803 }
804 else
805 {
806 // no new focus
807 int scrollDelta = maxJump;
808 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
809 {
810 scrollDelta = getScrollY();
811 }
812 else if (direction == View.FOCUS_DOWN)
813 {
814 if (getChildCount() > 0)
815 {
816 int daBottom = getChildAt(0).getBottom();
817 int screenBottom = getScrollY() + getHeight();
818 if (daBottom - screenBottom < maxJump)
819 {
820 scrollDelta = daBottom - screenBottom;
821 }
822 }
823 }
824 if (scrollDelta == 0)
825 {
826 return false;
827 }
828 doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
829 }
830 }
831 else
832 {
833 if (nextFocused != null)
834 {
835 nextFocused.getDrawingRect(mTempRect);
836 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
837 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
838 doScroll(scrollDelta, 0);
839 nextFocused.requestFocus(direction);
840 }
841 else
842 {
843 // no new focus
844 int scrollDelta = maxJump;
845 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
846 {
847 scrollDelta = getScrollY();
848 }
849 else if (direction == View.FOCUS_DOWN)
850 {
851 if (getChildCount() > 0)
852 {
853 int daBottom = getChildAt(0).getBottom();
854 int screenBottom = getScrollY() + getHeight();
855 if (daBottom - screenBottom < maxJump)
856 {
857 scrollDelta = daBottom - screenBottom;
858 }
859 }
860 }
861 if (scrollDelta == 0)
862 {
863 return false;
864 }
865 doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
866 }
867 }
868 return true;
869 }
870
876 private void doScroll(int deltaX, int deltaY)
877 {
878 if (deltaX != 0 || deltaY != 0)
879 {
880 smoothScrollBy(deltaX, deltaY);
881 }
882 }
883
890 public final void smoothScrollBy(int dx, int dy)
891 {
892 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
893 if (duration > ANIMATED_SCROLL_GAP)
894 {
895 mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
896 awakenScrollBars(mScroller.getDuration());
897 invalidate();
898 }
899 else
900 {
901 if (!mScroller.isFinished())
902 {
903 mScroller.abortAnimation();
904 }
905 scrollBy(dx, dy);
906 }
907 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
908 }
909
916 public final void smoothScrollTo(int x, int y)
917 {
918 smoothScrollBy(x - getScrollX(), y - getScrollY());
919 }
920
925 @Override protected int computeVerticalScrollRange()
926 {
927 int count = getChildCount();
928 return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
929 }
930
931 @Override protected int computeHorizontalScrollRange()
932 {
933 int count = getChildCount();
934 return count == 0 ? getWidth() : (getChildAt(0)).getRight();
935 }
936
937 @Override
938 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
939 {
940 ViewGroup.LayoutParams lp = child.getLayoutParams();
941 int childWidthMeasureSpec;
942 int childHeightMeasureSpec;
943
944 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
945 getPaddingLeft() + getPaddingRight(), lp.width);
946 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
947
948 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
949 }
950
951 @Override
952 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
953 int parentHeightMeasureSpec, int heightUsed)
954 {
955 final MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
956 final int childWidthMeasureSpec =
957 MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
958 final int childHeightMeasureSpec =
959 MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
960
961 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
962 }
963
964 @Override public void computeScroll()
965 {
966 if (mScroller.computeScrollOffset())
967 {
968 // This is called at drawing time by ViewGroup. We don't want to
969 // re-show the scrollbars at this point, which scrollTo will do,
970 // so we replicate most of scrollTo here.
971 //
972 // It's a little odd to call onScrollChanged from inside the drawing.
973 //
974 // It is, except when you remember that computeScroll() is used to
975 // animate scrolling. So unless we want to defer the onScrollChanged()
976 // until the end of the animated scrolling, we don't really have a
977 // choice here.
978 //
979 // I agree. The alternative, which I think would be worse, is to post
980 // something and tell the subclasses later. This is bad because there
981 // will be a window where mScrollX/Y is different from what the app
982 // thinks it is.
983 //
984 int oldX = getScrollX();
985 int oldY = getScrollY();
986 int x = mScroller.getCurrX();
987 int y = mScroller.getCurrY();
988 if (getChildCount() > 0)
989 {
990 View child = getChildAt(0);
991 scrollTo(
992 clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()),
993 clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(),
994 child.getHeight()));
995 }
996 else
997 {
998 scrollTo(x, y);
999 }
1000 if (oldX != getScrollX() || oldY != getScrollY())
1001 {
1002 onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
1003 }
1004
1005 // Keep on drawing until the animation has finished.
1006 postInvalidate();
1007 }
1008 }
1009
1015 private void scrollToChild(View child)
1016 {
1017 child.getDrawingRect(mTempRect);
1018 /* Offset from child's local coordinates to TwoDScrollView coordinates */
1019 offsetDescendantRectToMyCoords(child, mTempRect);
1020 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1021 if (scrollDelta != 0)
1022 {
1023 scrollBy(0, scrollDelta);
1024 }
1025 }
1026
1035 private boolean scrollToChildRect(Rect rect, boolean immediate)
1036 {
1037 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
1038 final boolean scroll = delta != 0;
1039 if (scroll)
1040 {
1041 if (immediate)
1042 {
1043 scrollBy(0, delta);
1044 }
1045 else
1046 {
1047 smoothScrollBy(0, delta);
1048 }
1049 }
1050 return scroll;
1051 }
1052
1062 {
1063 if (getChildCount() == 0)
1064 return 0;
1065 int height = getHeight();
1066 int screenTop = getScrollY();
1067 int screenBottom = screenTop + height;
1068 int fadingEdge = getVerticalFadingEdgeLength();
1069 // leave room for top fading edge as long as rect isn't at very top
1070 if (rect.top > 0)
1071 {
1072 screenTop += fadingEdge;
1073 }
1074
1075 // leave room for bottom fading edge as long as rect isn't at very bottom
1076 if (rect.bottom < getChildAt(0).getHeight())
1077 {
1078 screenBottom -= fadingEdge;
1079 }
1080 int scrollYDelta = 0;
1081 if (rect.bottom > screenBottom && rect.top > screenTop)
1082 {
1083 // need to move down to get it in view: move down just enough so
1084 // that the entire rectangle is in view (or at least the first
1085 // screen size chunk).
1086 if (rect.height() > height)
1087 {
1088 // just enough to get screen size chunk on
1089 scrollYDelta += (rect.top - screenTop);
1090 }
1091 else
1092 {
1093 // get entire rect at bottom of screen
1094 scrollYDelta += (rect.bottom - screenBottom);
1095 }
1096
1097 // make sure we aren't scrolling beyond the end of our content
1098 int bottom = getChildAt(0).getBottom();
1099 int distanceToBottom = bottom - screenBottom;
1100 scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
1101 }
1102 else if (rect.top < screenTop && rect.bottom < screenBottom)
1103 {
1104 // need to move up to get it in view: move up just enough so that
1105 // entire rectangle is in view (or at least the first screen
1106 // size chunk of it).
1107
1108 if (rect.height() > height)
1109 {
1110 // screen size chunk
1111 scrollYDelta -= (screenBottom - rect.bottom);
1112 }
1113 else
1114 {
1115 // entire rect at top
1116 scrollYDelta -= (screenTop - rect.top);
1117 }
1118
1119 // make sure we aren't scrolling any further than the top our content
1120 scrollYDelta = Math.max(scrollYDelta, -getScrollY());
1121 }
1122 return scrollYDelta;
1123 }
1124
1125 @Override public void requestChildFocus(View child, View focused)
1126 {
1127 if (!mTwoDScrollViewMovedFocus)
1128 {
1129 if (!mIsLayoutDirty)
1130 {
1131 scrollToChild(focused);
1132 }
1133 else
1134 {
1135 // The child may not be laid out yet, we can't compute the scroll yet
1136 mChildToScrollTo = focused;
1137 }
1138 }
1139 super.requestChildFocus(child, focused);
1140 }
1141
1149 @Override
1150 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)
1151 {
1152 // convert from forward / backward notation to up / down / left / right
1153 // (ugh).
1154 if (direction == View.FOCUS_FORWARD)
1155 {
1156 direction = View.FOCUS_DOWN;
1157 }
1158 else if (direction == View.FOCUS_BACKWARD)
1159 {
1160 direction = View.FOCUS_UP;
1161 }
1162
1163 final View nextFocus = previouslyFocusedRect == null
1164 ? FocusFinder.getInstance().findNextFocus(this, null, direction)
1165 : FocusFinder.getInstance().findNextFocusFromRect(
1166 this, previouslyFocusedRect, direction);
1167
1168 if (nextFocus == null)
1169 {
1170 return false;
1171 }
1172
1173 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1174 }
1175
1176 @Override
1177 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)
1178 {
1179 // offset into coordinate space of this scroll view
1180 rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
1181 return scrollToChildRect(rectangle, immediate);
1182 }
1183
1184 @Override public void requestLayout()
1185 {
1186 mIsLayoutDirty = true;
1187 super.requestLayout();
1188 }
1189
1190 @Override protected void onLayout(boolean changed, int l, int t, int r, int b)
1191 {
1192 super.onLayout(changed, l, t, r, b);
1193 mIsLayoutDirty = false;
1194 // Give a child focus if it needs it
1195 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this))
1196 {
1197 scrollToChild(mChildToScrollTo);
1198 }
1199 mChildToScrollTo = null;
1200
1201 // Calling this with the present values causes it to re-clam them
1202 scrollTo(getScrollX(), getScrollY());
1203 }
1204
1205 @Override protected void onSizeChanged(int w, int h, int oldw, int oldh)
1206 {
1207 super.onSizeChanged(w, h, oldw, oldh);
1208
1209 View currentFocused = findFocus();
1210 if (null == currentFocused || this == currentFocused)
1211 return;
1212
1213 // If the currently-focused view was visible on the screen when the
1214 // screen was at the old height, then scroll the screen to make that
1215 // view visible with the new screen height.
1216 currentFocused.getDrawingRect(mTempRect);
1217 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1218 int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1219 int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1220 doScroll(scrollDeltaX, scrollDeltaY);
1221 }
1222
1226 private boolean isViewDescendantOf(View child, View parent)
1227 {
1228 if (child == parent)
1229 {
1230 return true;
1231 }
1232
1233 final ViewParent theParent = child.getParent();
1234 return (theParent instanceof ViewGroup) && isViewDescendantOf((View)theParent, parent);
1235 }
1236
1244 public void fling(int velocityX, int velocityY)
1245 {
1246 if (getChildCount() > 0)
1247 {
1248 int height = getHeight() - getPaddingBottom() - getPaddingTop();
1249 int bottom = getChildAt(0).getHeight();
1250 int width = getWidth() - getPaddingRight() - getPaddingLeft();
1251 int right = getChildAt(0).getWidth();
1252
1253 mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0,
1254 bottom - height);
1255
1256 final boolean movingDown = velocityY > 0;
1257 final boolean movingRight = velocityX > 0;
1258
1259 View newFocused = findFocusableViewInMyBounds(
1260 movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus());
1261 if (newFocused == null)
1262 {
1263 newFocused = this;
1264 }
1265
1266 if (newFocused != findFocus() &&
1267 newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP))
1268 {
1269 mTwoDScrollViewMovedFocus = true;
1270 mTwoDScrollViewMovedFocus = false;
1271 }
1272
1273 awakenScrollBars(mScroller.getDuration());
1274 invalidate();
1275 }
1276 }
1277
1283 public void scrollTo(int x, int y)
1284 {
1285 // we rely on the fact the View.scrollBy calls scrollTo.
1286 if (getChildCount() > 0)
1287 {
1288 View child = getChildAt(0);
1289 x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
1290 y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
1291 if (x != getScrollX() || y != getScrollY())
1292 {
1293 super.scrollTo(x, y);
1294 }
1295 }
1296 }
1297
1298 private int clamp(int n, int my, int child)
1299 {
1300 if (my >= child || n < 0)
1301 {
1302 /* my >= child is this case:
1303 * |--------------- me ---------------|
1304 * |------ child ------|
1305 * or
1306 * |--------------- me ---------------|
1307 * |------ child ------|
1308 * or
1309 * |--------------- me ---------------|
1310 * |------ child ------|
1311 *
1312 * n < 0 is this case:
1313 * |------ me ------|
1314 * |-------- child --------|
1315 * |-- mScrollX --|
1316 */
1317 return 0;
1318 }
1319 if ((my + n) > child)
1320 {
1321 /* this case:
1322 * |------ me ------|
1323 * |------ child ------|
1324 * |-- mScrollX --|
1325 */
1326 return child - my;
1327 }
1328 return n;
1329 }
1330
1331 public void setScrollViewListener(ScrollView2DListener scrollViewListener)
1332 {
1333 this.scrollView2DListener = scrollViewListener;
1334 }
1335
1336 @Override protected void onScrollChanged(int x, int y, int oldx, int oldy)
1337 {
1338 super.onScrollChanged(x, y, oldx, oldy);
1339 if (scrollView2DListener != null)
1340 {
1341 scrollView2DListener.onScrollChanged(this, x, y, oldx, oldy);
1342 }
1343 }
1344
1345 // interface to receive notifications when the view is scrolled
1346 public interface ScrollView2DListener {
1347 void onScrollChanged(ScrollView2D scrollView, int x, int y, int oldx, int oldy);
1348 }
1349}
boolean arrowScroll(int direction, boolean horizontal)
boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)
boolean fullScroll(int direction, boolean horizontal)