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