FreeRDP
Loading...
Searching...
No Matches
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.MotionEvent;
29import android.view.VelocityTracker;
30import android.view.View;
31import android.view.ViewConfiguration;
32import android.view.ViewGroup;
33import android.view.ViewParent;
34import android.view.animation.AnimationUtils;
35import android.widget.FrameLayout;
36import android.widget.LinearLayout;
37import android.widget.Scroller;
38import android.widget.TextView;
39
40import java.util.List;
41
56public class ScrollView2D extends FrameLayout
57{
58
59 static final int ANIMATED_SCROLL_GAP = 250;
60 static final float MAX_SCROLL_FACTOR = 0.5f;
61 private final Rect mTempRect = new Rect();
62 private ScrollView2DListener scrollView2DListener = null;
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;
104 public ScrollView2D(Context context)
105 {
106 super(context);
107 initTwoDScrollView();
108 }
109
110 public ScrollView2D(Context context, AttributeSet attrs)
111 {
112 super(context, attrs);
113 initTwoDScrollView();
114 }
115
116 public ScrollView2D(Context context, AttributeSet attrs, int defStyle)
117 {
118 super(context, attrs, defStyle);
119 initTwoDScrollView();
120 }
121
122 @Override protected float getTopFadingEdgeStrength()
123 {
124 if (getChildCount() == 0)
125 {
126 return 0.0f;
127 }
128 final int length = getVerticalFadingEdgeLength();
129 if (getScrollY() < length)
130 {
131 return getScrollY() / (float)length;
132 }
133 return 1.0f;
134 }
135
136 @Override protected float getBottomFadingEdgeStrength()
137 {
138 if (getChildCount() == 0)
139 {
140 return 0.0f;
141 }
142 final int length = getVerticalFadingEdgeLength();
143 final int bottomEdge = getHeight() - getPaddingBottom();
144 final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
145 if (span < length)
146 {
147 return span / (float)length;
148 }
149 return 1.0f;
150 }
151
152 @Override protected float getLeftFadingEdgeStrength()
153 {
154 if (getChildCount() == 0)
155 {
156 return 0.0f;
157 }
158 final int length = getHorizontalFadingEdgeLength();
159 if (getScrollX() < length)
160 {
161 return getScrollX() / (float)length;
162 }
163 return 1.0f;
164 }
165
166 @Override protected float getRightFadingEdgeStrength()
167 {
168 if (getChildCount() == 0)
169 {
170 return 0.0f;
171 }
172 final int length = getHorizontalFadingEdgeLength();
173 final int rightEdge = getWidth() - getPaddingRight();
174 final int span = getChildAt(0).getRight() - getScrollX() - rightEdge;
175 if (span < length)
176 {
177 return span / (float)length;
178 }
179 return 1.0f;
180 }
181
185 public void setScrollEnabled(boolean enable)
186 {
187 scrollEnabled = enable;
188 }
189
195 {
196 return (int)(MAX_SCROLL_FACTOR * getHeight());
197 }
198
199 public int getMaxScrollAmountHorizontal()
200 {
201 return (int)(MAX_SCROLL_FACTOR * getWidth());
202 }
203
204 private void initTwoDScrollView()
205 {
206 mScroller = new Scroller(getContext());
207 setFocusable(true);
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();
214 }
215
216 @Override public void addView(View child)
217 {
218 if (getChildCount() > 0)
219 {
220 throw new IllegalStateException("TwoDScrollView can host only one direct child");
221 }
222 super.addView(child);
223 }
224
225 @Override public void addView(View child, int index)
226 {
227 if (getChildCount() > 0)
228 {
229 throw new IllegalStateException("TwoDScrollView can host only one direct child");
230 }
231 super.addView(child, index);
232 }
233
234 @Override public void addView(View child, ViewGroup.LayoutParams params)
235 {
236 if (getChildCount() > 0)
237 {
238 throw new IllegalStateException("TwoDScrollView can host only one direct child");
239 }
240 super.addView(child, params);
241 }
242
243 @Override public void addView(View child, int index, ViewGroup.LayoutParams params)
244 {
245 if (getChildCount() > 0)
246 {
247 throw new IllegalStateException("TwoDScrollView can host only one direct child");
248 }
249 super.addView(child, index, params);
250 }
251
255 private boolean canScroll()
256 {
257 if (!scrollEnabled)
258 return false;
259 View child = getChildAt(0);
260 if (child != null)
261 {
262 int childHeight = child.getHeight();
263 int childWidth = child.getWidth();
264 return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) ||
265 (getWidth() < childWidth + getPaddingLeft() + getPaddingRight());
266 }
267 return false;
268 }
269
270 @Override public boolean onInterceptTouchEvent(MotionEvent ev)
271 {
272 /*
273 * This method JUST determines whether we want to intercept the motion.
274 * If we return true, onMotionEvent will be called and we do the actual
275 * scrolling there.
276 *
277 * Shortcut the most recurring case: the user is in the dragging
278 * state and he is moving his finger. We want to intercept this
279 * motion.
280 */
281 final int action = ev.getAction();
282 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged))
283 {
284 return true;
285 }
286 if (!canScroll())
287 {
288 mIsBeingDragged = false;
289 return false;
290 }
291 final float y = ev.getY();
292 final float x = ev.getX();
293 switch (action)
294 {
295 case MotionEvent.ACTION_MOVE:
296 /*
297 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
298 * whether the user has moved far enough from his original down touch.
299 */
300 /*
301 * Locally do absolute value. mLastMotionY is set to the y value
302 * of the down event.
303 */
304 final int yDiff = (int)Math.abs(y - mLastMotionY);
305 final int xDiff = (int)Math.abs(x - mLastMotionX);
306 if (yDiff > mTouchSlop || xDiff > mTouchSlop)
307 {
308 mIsBeingDragged = true;
309 }
310 break;
311
312 case MotionEvent.ACTION_DOWN:
313 /* Remember location of down touch */
314 mLastMotionY = y;
315 mLastMotionX = x;
316
317 /*
318 * If being flinged and user touches the screen, initiate drag;
319 * otherwise don't. mScroller.isFinished should be false when
320 * being flinged.
321 */
322 mIsBeingDragged = !mScroller.isFinished();
323 break;
324
325 case MotionEvent.ACTION_CANCEL:
326 case MotionEvent.ACTION_UP:
327 /* Release the drag */
328 mIsBeingDragged = false;
329 break;
330 }
331
332 /*
333 * The only time we want to intercept motion events is if we are in the
334 * drag mode.
335 */
336 return mIsBeingDragged;
337 }
338
339 @Override public boolean onTouchEvent(MotionEvent ev)
340 {
341
342 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0)
343 {
344 // Don't handle edge touches immediately -- they may actually belong to one of our
345 // descendants.
346 return false;
347 }
348
349 if (!canScroll())
350 {
351 return false;
352 }
353
354 if (mVelocityTracker == null)
355 {
356 mVelocityTracker = VelocityTracker.obtain();
357 }
358 mVelocityTracker.addMovement(ev);
359
360 final int action = ev.getAction();
361 final float y = ev.getY();
362 final float x = ev.getX();
363
364 switch (action)
365 {
366 case MotionEvent.ACTION_DOWN:
367 /*
368 * If being flinged and user touches, stop the fling. isFinished
369 * will be false if being flinged.
370 */
371 if (!mScroller.isFinished())
372 {
373 mScroller.abortAnimation();
374 }
375
376 // Remember where the motion event started
377 mLastMotionY = y;
378 mLastMotionX = x;
379 break;
380 case MotionEvent.ACTION_MOVE:
381 // Scroll to follow the motion event
382 int deltaX = (int)(mLastMotionX - x);
383 int deltaY = (int)(mLastMotionY - y);
384 mLastMotionX = x;
385 mLastMotionY = y;
386
387 if (deltaX < 0)
388 {
389 if (getScrollX() < 0)
390 {
391 deltaX = 0;
392 }
393 }
394 else if (deltaX > 0)
395 {
396 final int rightEdge = getWidth() - getPaddingRight();
397 final int availableToScroll =
398 getChildAt(0).getRight() - getScrollX() - rightEdge;
399 if (availableToScroll > 0)
400 {
401 deltaX = Math.min(availableToScroll, deltaX);
402 }
403 else
404 {
405 deltaX = 0;
406 }
407 }
408 if (deltaY < 0)
409 {
410 if (getScrollY() < 0)
411 {
412 deltaY = 0;
413 }
414 }
415 else if (deltaY > 0)
416 {
417 final int bottomEdge = getHeight() - getPaddingBottom();
418 final int availableToScroll =
419 getChildAt(0).getBottom() - getScrollY() - bottomEdge;
420 if (availableToScroll > 0)
421 {
422 deltaY = Math.min(availableToScroll, deltaY);
423 }
424 else
425 {
426 deltaY = 0;
427 }
428 }
429 if (deltaY != 0 || deltaX != 0)
430 scrollBy(deltaX, deltaY);
431 break;
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) &&
438 getChildCount() > 0)
439 {
440 fling(-initialXVelocity, -initialYVelocity);
441 }
442 if (mVelocityTracker != null)
443 {
444 mVelocityTracker.recycle();
445 mVelocityTracker = null;
446 }
447 }
448 return true;
449 }
450
466 private View findFocusableViewInMyBounds(final boolean topFocus, final int top,
467 final boolean leftFocus, final int left,
468 View preferredFocusable)
469 {
470 /*
471 * The fading edge's transparent side should be considered for focus
472 * since it's mostly visible, so we divide the actual fading edge length
473 * by 2.
474 */
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;
481
482 if ((preferredFocusable != null) &&
483 (preferredFocusable.getTop() < bottomWithoutFadingEdge) &&
484 (preferredFocusable.getBottom() > topWithoutFadingEdge) &&
485 (preferredFocusable.getLeft() < rightWithoutFadingEdge) &&
486 (preferredFocusable.getRight() > leftWithoutFadingEdge))
487 {
488 return preferredFocusable;
489 }
490 return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge,
491 leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
492 }
493
508 private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus,
509 int left, int right)
510 {
511 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
512 View focusCandidate = null;
513
514 /*
515 * A fully contained focusable is one where its top is below the bound's
516 * top, and its bottom is above the bound's bottom. A partially
517 * contained focusable is one where some part of it is within the
518 * bounds, but it also has some part that is not within bounds. A fully contained
519 * focusable is preferred to a partially contained focusable.
520 */
521 boolean foundFullyContainedFocusable = false;
522
523 int count = focusables.size();
524 for (int i = 0; i < count; i++)
525 {
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();
531
532 if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right)
533 {
534 /*
535 * the focusable is in the target area, it is a candidate for
536 * focusing
537 */
538 final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) &&
539 (left < viewLeft) && (viewRight < right);
540 if (focusCandidate == null)
541 {
542 /* No candidate, take this one */
543 focusCandidate = view;
544 foundFullyContainedFocusable = viewIsFullyContained;
545 }
546 else
547 {
548 final boolean viewIsCloserToVerticalBoundary =
549 (topFocus && viewTop < focusCandidate.getTop()) ||
550 (!topFocus && viewBottom > focusCandidate.getBottom());
551 final boolean viewIsCloserToHorizontalBoundary =
552 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
553 (!leftFocus && viewRight > focusCandidate.getRight());
554 if (foundFullyContainedFocusable)
555 {
556 if (viewIsFullyContained && viewIsCloserToVerticalBoundary &&
557 viewIsCloserToHorizontalBoundary)
558 {
559 /*
560 * We're dealing with only fully contained views, so
561 * it has to be closer to the boundary to beat our
562 * candidate
563 */
564 focusCandidate = view;
565 }
566 }
567 else
568 {
569 if (viewIsFullyContained)
570 {
571 /* Any fully contained view beats a partially contained view */
572 focusCandidate = view;
573 foundFullyContainedFocusable = true;
574 }
575 else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary)
576 {
577 /*
578 * Partially contained view beats another partially
579 * contained view if it's closer
580 */
581 focusCandidate = view;
582 }
583 }
584 }
585 }
586 }
587 return focusCandidate;
588 }
589
602 public boolean fullScroll(int direction, boolean horizontal)
603 {
604 if (!horizontal)
605 {
606 boolean down = direction == View.FOCUS_DOWN;
607 int height = getHeight();
608 mTempRect.top = 0;
609 mTempRect.bottom = height;
610 if (down)
611 {
612 int count = getChildCount();
613 if (count > 0)
614 {
615 View view = getChildAt(count - 1);
616 mTempRect.bottom = view.getBottom();
617 mTempRect.top = mTempRect.bottom - height;
618 }
619 }
620 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0);
621 }
622 else
623 {
624 boolean right = direction == View.FOCUS_DOWN;
625 int width = getWidth();
626 mTempRect.left = 0;
627 mTempRect.right = width;
628 if (right)
629 {
630 int count = getChildCount();
631 if (count > 0)
632 {
633 View view = getChildAt(count - 1);
634 mTempRect.right = view.getBottom();
635 mTempRect.left = mTempRect.right - width;
636 }
637 }
638 return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom);
639 }
640 }
641
655 private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left,
656 int right)
657 {
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)
669 {
670 newFocused = this;
671 }
672 if ((top >= containerTop && bottom <= containerBottom) ||
673 (left >= containerLeft && right <= containerRight))
674 {
675 handled = false;
676 }
677 else
678 {
679 int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
680 int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
681 doScroll(deltaX, deltaY);
682 }
683 if (newFocused != findFocus() && newFocused.requestFocus(directionY))
684 {
685 mTwoDScrollViewMovedFocus = true;
686 mTwoDScrollViewMovedFocus = false;
687 }
688 return handled;
689 }
690
698 public boolean arrowScroll(int direction, boolean horizontal)
699 {
700 View currentFocused = findFocus();
701 if (currentFocused == this)
702 currentFocused = null;
703 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
704 final int maxJump =
705 horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical();
706
707 if (!horizontal)
708 {
709 if (nextFocused != null)
710 {
711 nextFocused.getDrawingRect(mTempRect);
712 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
713 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
714 doScroll(0, scrollDelta);
715 nextFocused.requestFocus(direction);
716 }
717 else
718 {
719 // no new focus
720 int scrollDelta = maxJump;
721 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
722 {
723 scrollDelta = getScrollY();
724 }
725 else if (direction == View.FOCUS_DOWN)
726 {
727 if (getChildCount() > 0)
728 {
729 int daBottom = getChildAt(0).getBottom();
730 int screenBottom = getScrollY() + getHeight();
731 if (daBottom - screenBottom < maxJump)
732 {
733 scrollDelta = daBottom - screenBottom;
734 }
735 }
736 }
737 if (scrollDelta == 0)
738 {
739 return false;
740 }
741 doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
742 }
743 }
744 else
745 {
746 if (nextFocused != null)
747 {
748 nextFocused.getDrawingRect(mTempRect);
749 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
750 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
751 doScroll(scrollDelta, 0);
752 nextFocused.requestFocus(direction);
753 }
754 else
755 {
756 // no new focus
757 int scrollDelta = maxJump;
758 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
759 {
760 scrollDelta = getScrollY();
761 }
762 else if (direction == View.FOCUS_DOWN)
763 {
764 if (getChildCount() > 0)
765 {
766 int daBottom = getChildAt(0).getBottom();
767 int screenBottom = getScrollY() + getHeight();
768 if (daBottom - screenBottom < maxJump)
769 {
770 scrollDelta = daBottom - screenBottom;
771 }
772 }
773 }
774 if (scrollDelta == 0)
775 {
776 return false;
777 }
778 doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
779 }
780 }
781 return true;
782 }
783
789 private void doScroll(int deltaX, int deltaY)
790 {
791 if (deltaX != 0 || deltaY != 0)
792 {
793 smoothScrollBy(deltaX, deltaY);
794 }
795 }
796
803 public final void smoothScrollBy(int dx, int dy)
804 {
805 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
806 if (duration > ANIMATED_SCROLL_GAP)
807 {
808 mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
809 awakenScrollBars(mScroller.getDuration());
810 invalidate();
811 }
812 else
813 {
814 if (!mScroller.isFinished())
815 {
816 mScroller.abortAnimation();
817 }
818 scrollBy(dx, dy);
819 }
820 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
821 }
822
829 public final void smoothScrollTo(int x, int y)
830 {
831 smoothScrollBy(x - getScrollX(), y - getScrollY());
832 }
833
838 @Override protected int computeVerticalScrollRange()
839 {
840 int count = getChildCount();
841 return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
842 }
843
844 @Override protected int computeHorizontalScrollRange()
845 {
846 int count = getChildCount();
847 return count == 0 ? getWidth() : (getChildAt(0)).getRight();
848 }
849
850 @Override
851 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
852 {
853 ViewGroup.LayoutParams lp = child.getLayoutParams();
854 int childWidthMeasureSpec;
855 int childHeightMeasureSpec;
856
857 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
858 getPaddingLeft() + getPaddingRight(), lp.width);
859 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
860
861 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
862 }
863
864 @Override
865 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
866 int parentHeightMeasureSpec, int heightUsed)
867 {
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);
873
874 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
875 }
876
877 @Override public void computeScroll()
878 {
879 if (mScroller.computeScrollOffset())
880 {
881 // This is called at drawing time by ViewGroup. We don't want to
882 // re-show the scrollbars at this point, which scrollTo will do,
883 // so we replicate most of scrollTo here.
884 //
885 // It's a little odd to call onScrollChanged from inside the drawing.
886 //
887 // It is, except when you remember that computeScroll() is used to
888 // animate scrolling. So unless we want to defer the onScrollChanged()
889 // until the end of the animated scrolling, we don't really have a
890 // choice here.
891 //
892 // I agree. The alternative, which I think would be worse, is to post
893 // something and tell the subclasses later. This is bad because there
894 // will be a window where mScrollX/Y is different from what the app
895 // thinks it is.
896 //
897 int oldX = getScrollX();
898 int oldY = getScrollY();
899 int x = mScroller.getCurrX();
900 int y = mScroller.getCurrY();
901 if (getChildCount() > 0)
902 {
903 View child = getChildAt(0);
904 scrollTo(
905 clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()),
906 clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(),
907 child.getHeight()));
908 }
909 else
910 {
911 scrollTo(x, y);
912 }
913 if (oldX != getScrollX() || oldY != getScrollY())
914 {
915 onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
916 }
917
918 // Keep on drawing until the animation has finished.
919 postInvalidate();
920 }
921 }
922
928 private void scrollToChild(View child)
929 {
930 child.getDrawingRect(mTempRect);
931 /* Offset from child's local coordinates to TwoDScrollView coordinates */
932 offsetDescendantRectToMyCoords(child, mTempRect);
933 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
934 if (scrollDelta != 0)
935 {
936 scrollBy(0, scrollDelta);
937 }
938 }
939
948 private boolean scrollToChildRect(Rect rect, boolean immediate)
949 {
950 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
951 final boolean scroll = delta != 0;
952 if (scroll)
953 {
954 if (immediate)
955 {
956 scrollBy(0, delta);
957 }
958 else
959 {
960 smoothScrollBy(0, delta);
961 }
962 }
963 return scroll;
964 }
965
975 {
976 if (getChildCount() == 0)
977 return 0;
978 int height = getHeight();
979 int screenTop = getScrollY();
980 int screenBottom = screenTop + height;
981 int fadingEdge = getVerticalFadingEdgeLength();
982 // leave room for top fading edge as long as rect isn't at very top
983 if (rect.top > 0)
984 {
985 screenTop += fadingEdge;
986 }
987
988 // leave room for bottom fading edge as long as rect isn't at very bottom
989 if (rect.bottom < getChildAt(0).getHeight())
990 {
991 screenBottom -= fadingEdge;
992 }
993 int scrollYDelta = 0;
994 if (rect.bottom > screenBottom && rect.top > screenTop)
995 {
996 // need to move down to get it in view: move down just enough so
997 // that the entire rectangle is in view (or at least the first
998 // screen size chunk).
999 if (rect.height() > height)
1000 {
1001 // just enough to get screen size chunk on
1002 scrollYDelta += (rect.top - screenTop);
1003 }
1004 else
1005 {
1006 // get entire rect at bottom of screen
1007 scrollYDelta += (rect.bottom - screenBottom);
1008 }
1009
1010 // make sure we aren't scrolling beyond the end of our content
1011 int bottom = getChildAt(0).getBottom();
1012 int distanceToBottom = bottom - screenBottom;
1013 scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
1014 }
1015 else if (rect.top < screenTop && rect.bottom < screenBottom)
1016 {
1017 // need to move up to get it in view: move up just enough so that
1018 // entire rectangle is in view (or at least the first screen
1019 // size chunk of it).
1020
1021 if (rect.height() > height)
1022 {
1023 // screen size chunk
1024 scrollYDelta -= (screenBottom - rect.bottom);
1025 }
1026 else
1027 {
1028 // entire rect at top
1029 scrollYDelta -= (screenTop - rect.top);
1030 }
1031
1032 // make sure we aren't scrolling any further than the top our content
1033 scrollYDelta = Math.max(scrollYDelta, -getScrollY());
1034 }
1035 return scrollYDelta;
1036 }
1037
1038 @Override public void requestChildFocus(View child, View focused)
1039 {
1040 if (!mTwoDScrollViewMovedFocus)
1041 {
1042 if (!mIsLayoutDirty)
1043 {
1044 scrollToChild(focused);
1045 }
1046 else
1047 {
1048 // The child may not be laid out yet, we can't compute the scroll yet
1049 mChildToScrollTo = focused;
1050 }
1051 }
1052 super.requestChildFocus(child, focused);
1053 }
1054
1062 @Override
1063 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)
1064 {
1065 // convert from forward / backward notation to up / down / left / right
1066 // (ugh).
1067 if (direction == View.FOCUS_FORWARD)
1068 {
1069 direction = View.FOCUS_DOWN;
1070 }
1071 else if (direction == View.FOCUS_BACKWARD)
1072 {
1073 direction = View.FOCUS_UP;
1074 }
1075
1076 final View nextFocus = previouslyFocusedRect == null
1077 ? FocusFinder.getInstance().findNextFocus(this, null, direction)
1078 : FocusFinder.getInstance().findNextFocusFromRect(
1079 this, previouslyFocusedRect, direction);
1080
1081 if (nextFocus == null)
1082 {
1083 return false;
1084 }
1085
1086 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1087 }
1088
1089 @Override
1090 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)
1091 {
1092 // offset into coordinate space of this scroll view
1093 rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
1094 return scrollToChildRect(rectangle, immediate);
1095 }
1096
1097 @Override public void requestLayout()
1098 {
1099 mIsLayoutDirty = true;
1100 super.requestLayout();
1101 }
1102
1103 @Override protected void onLayout(boolean changed, int l, int t, int r, int b)
1104 {
1105 super.onLayout(changed, l, t, r, b);
1106 mIsLayoutDirty = false;
1107 // Give a child focus if it needs it
1108 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this))
1109 {
1110 scrollToChild(mChildToScrollTo);
1111 }
1112 mChildToScrollTo = null;
1113
1114 // Center the content area (excluding touch-pointer padding) within the viewport.
1115 // This keeps the RDP surface visually centered regardless of pointer padding changes.
1116 if (getChildCount() > 0)
1117 {
1118 View child = getChildAt(0);
1119 int ptw = 0, pth = 0;
1120 if (child instanceof SessionView)
1121 {
1122 ptw = ((SessionView)child).getTouchPointerPaddingWidth();
1123 pth = ((SessionView)child).getTouchPointerPaddingHeight();
1124 }
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());
1133 }
1134
1135 // Calling this with the present values causes it to re-clamp them
1136 scrollTo(getScrollX(), getScrollY());
1137 }
1138
1139 @Override protected void onSizeChanged(int w, int h, int oldw, int oldh)
1140 {
1141 super.onSizeChanged(w, h, oldw, oldh);
1142
1143 View currentFocused = findFocus();
1144 if (null == currentFocused || this == currentFocused)
1145 return;
1146
1147 // If the currently-focused view was visible on the screen when the
1148 // screen was at the old height, then scroll the screen to make that
1149 // view visible with the new screen height.
1150 currentFocused.getDrawingRect(mTempRect);
1151 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1152 int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1153 int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1154 doScroll(scrollDeltaX, scrollDeltaY);
1155 }
1156
1160 private boolean isViewDescendantOf(View child, View parent)
1161 {
1162 if (child == parent)
1163 {
1164 return true;
1165 }
1166
1167 final ViewParent theParent = child.getParent();
1168 return (theParent instanceof ViewGroup) && isViewDescendantOf((View)theParent, parent);
1169 }
1170
1178 public void fling(int velocityX, int velocityY)
1179 {
1180 if (getChildCount() > 0)
1181 {
1182 int height = getHeight() - getPaddingBottom() - getPaddingTop();
1183 int bottom = getChildAt(0).getHeight();
1184 int width = getWidth() - getPaddingRight() - getPaddingLeft();
1185 int right = getChildAt(0).getWidth();
1186
1187 mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0,
1188 bottom - height);
1189
1190 final boolean movingDown = velocityY > 0;
1191 final boolean movingRight = velocityX > 0;
1192
1193 View newFocused = findFocusableViewInMyBounds(
1194 movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus());
1195 if (newFocused == null)
1196 {
1197 newFocused = this;
1198 }
1199
1200 if (newFocused != findFocus() &&
1201 newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP))
1202 {
1203 mTwoDScrollViewMovedFocus = true;
1204 mTwoDScrollViewMovedFocus = false;
1205 }
1206
1207 awakenScrollBars(mScroller.getDuration());
1208 invalidate();
1209 }
1210 }
1211
1217 public void scrollTo(int x, int y)
1218 {
1219 // we rely on the fact the View.scrollBy calls scrollTo.
1220 if (getChildCount() > 0)
1221 {
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())
1226 {
1227 super.scrollTo(x, y);
1228 }
1229 }
1230 }
1231
1232 private int clamp(int n, int my, int child)
1233 {
1234 if (my >= child || n < 0)
1235 {
1236 /* my >= child is this case:
1237 * |--------------- me ---------------|
1238 * |------ child ------|
1239 * or
1240 * |--------------- me ---------------|
1241 * |------ child ------|
1242 * or
1243 * |--------------- me ---------------|
1244 * |------ child ------|
1245 *
1246 * n < 0 is this case:
1247 * |------ me ------|
1248 * |-------- child --------|
1249 * |-- mScrollX --|
1250 */
1251 return 0;
1252 }
1253 if ((my + n) > child)
1254 {
1255 /* this case:
1256 * |------ me ------|
1257 * |------ child ------|
1258 * |-- mScrollX --|
1259 */
1260 return child - my;
1261 }
1262 return n;
1263 }
1264
1265 public void setScrollViewListener(ScrollView2DListener scrollViewListener)
1266 {
1267 this.scrollView2DListener = scrollViewListener;
1268 }
1269
1270 @Override protected void onScrollChanged(int x, int y, int oldx, int oldy)
1271 {
1272 super.onScrollChanged(x, y, oldx, oldy);
1273 if (scrollView2DListener != null)
1274 {
1275 scrollView2DListener.onScrollChanged(this, x, y, oldx, oldy);
1276 }
1277 }
1278
1279 // interface to receive notifications when the view is scrolled
1280 public interface ScrollView2DListener {
1281 void onScrollChanged(ScrollView2D scrollView, int x, int y, int oldx, int oldy);
1282 }
1283}
boolean arrowScroll(int direction, boolean horizontal)
boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)
boolean fullScroll(int direction, boolean horizontal)