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 // Let child handle touchpad scroll gestures.
284 if (ev.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE)
285 {
286 mIsBeingDragged = false;
287 return false;
288 }
289
290 final int action = ev.getAction();
291 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged))
292 {
293 return true;
294 }
295 if (!canScroll())
296 {
297 mIsBeingDragged = false;
298 return false;
299 }
300 final float y = ev.getY();
301 final float x = ev.getX();
302 switch (action)
303 {
304 case MotionEvent.ACTION_MOVE:
305 /*
306 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
307 * whether the user has moved far enough from his original down touch.
308 */
309 /*
310 * Locally do absolute value. mLastMotionY is set to the y value
311 * of the down event.
312 */
313 final int yDiff = (int)Math.abs(y - mLastMotionY);
314 final int xDiff = (int)Math.abs(x - mLastMotionX);
315 if (yDiff > mTouchSlop || xDiff > mTouchSlop)
316 {
317 mIsBeingDragged = true;
318 }
319 break;
320
321 case MotionEvent.ACTION_DOWN:
322 /* Remember location of down touch */
323 mLastMotionY = y;
324 mLastMotionX = x;
325
326 /*
327 * If being flinged and user touches the screen, initiate drag;
328 * otherwise don't. mScroller.isFinished should be false when
329 * being flinged.
330 */
331 mIsBeingDragged = !mScroller.isFinished();
332 break;
333
334 case MotionEvent.ACTION_CANCEL:
335 case MotionEvent.ACTION_UP:
336 /* Release the drag */
337 mIsBeingDragged = false;
338 break;
339 }
340
341 /*
342 * The only time we want to intercept motion events is if we are in the
343 * drag mode.
344 */
345 return mIsBeingDragged;
346 }
347
348 @Override public boolean onTouchEvent(MotionEvent ev)
349 {
350
351 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0)
352 {
353 // Don't handle edge touches immediately -- they may actually belong to one of our
354 // descendants.
355 return false;
356 }
357
358 if (!canScroll())
359 {
360 return false;
361 }
362
363 if (mVelocityTracker == null)
364 {
365 mVelocityTracker = VelocityTracker.obtain();
366 }
367 mVelocityTracker.addMovement(ev);
368
369 final int action = ev.getAction();
370 final float y = ev.getY();
371 final float x = ev.getX();
372
373 switch (action)
374 {
375 case MotionEvent.ACTION_DOWN:
376 /*
377 * If being flinged and user touches, stop the fling. isFinished
378 * will be false if being flinged.
379 */
380 if (!mScroller.isFinished())
381 {
382 mScroller.abortAnimation();
383 }
384
385 // Remember where the motion event started
386 mLastMotionY = y;
387 mLastMotionX = x;
388 break;
389 case MotionEvent.ACTION_MOVE:
390 // Scroll to follow the motion event
391 int deltaX = (int)(mLastMotionX - x);
392 int deltaY = (int)(mLastMotionY - y);
393 mLastMotionX = x;
394 mLastMotionY = y;
395
396 if (deltaX < 0)
397 {
398 if (getScrollX() < 0)
399 {
400 deltaX = 0;
401 }
402 }
403 else if (deltaX > 0)
404 {
405 final int rightEdge = getWidth() - getPaddingRight();
406 final int availableToScroll =
407 getChildAt(0).getRight() - getScrollX() - rightEdge;
408 if (availableToScroll > 0)
409 {
410 deltaX = Math.min(availableToScroll, deltaX);
411 }
412 else
413 {
414 deltaX = 0;
415 }
416 }
417 if (deltaY < 0)
418 {
419 if (getScrollY() < 0)
420 {
421 deltaY = 0;
422 }
423 }
424 else if (deltaY > 0)
425 {
426 final int bottomEdge = getHeight() - getPaddingBottom();
427 final int availableToScroll =
428 getChildAt(0).getBottom() - getScrollY() - bottomEdge;
429 if (availableToScroll > 0)
430 {
431 deltaY = Math.min(availableToScroll, deltaY);
432 }
433 else
434 {
435 deltaY = 0;
436 }
437 }
438 if (deltaY != 0 || deltaX != 0)
439 scrollBy(deltaX, deltaY);
440 break;
441 case MotionEvent.ACTION_UP:
442 final VelocityTracker velocityTracker = mVelocityTracker;
443 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
444 int initialXVelocity = (int)velocityTracker.getXVelocity();
445 int initialYVelocity = (int)velocityTracker.getYVelocity();
446 if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) &&
447 getChildCount() > 0)
448 {
449 fling(-initialXVelocity, -initialYVelocity);
450 }
451 if (mVelocityTracker != null)
452 {
453 mVelocityTracker.recycle();
454 mVelocityTracker = null;
455 }
456 }
457 return true;
458 }
459
475 private View findFocusableViewInMyBounds(final boolean topFocus, final int top,
476 final boolean leftFocus, final int left,
477 View preferredFocusable)
478 {
479 /*
480 * The fading edge's transparent side should be considered for focus
481 * since it's mostly visible, so we divide the actual fading edge length
482 * by 2.
483 */
484 final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2;
485 final int topWithoutFadingEdge = top + verticalFadingEdgeLength;
486 final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength;
487 final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
488 final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength;
489 final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength;
490
491 if ((preferredFocusable != null) &&
492 (preferredFocusable.getTop() < bottomWithoutFadingEdge) &&
493 (preferredFocusable.getBottom() > topWithoutFadingEdge) &&
494 (preferredFocusable.getLeft() < rightWithoutFadingEdge) &&
495 (preferredFocusable.getRight() > leftWithoutFadingEdge))
496 {
497 return preferredFocusable;
498 }
499 return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge,
500 leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
501 }
502
517 private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus,
518 int left, int right)
519 {
520 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
521 View focusCandidate = null;
522
523 /*
524 * A fully contained focusable is one where its top is below the bound's
525 * top, and its bottom is above the bound's bottom. A partially
526 * contained focusable is one where some part of it is within the
527 * bounds, but it also has some part that is not within bounds. A fully contained
528 * focusable is preferred to a partially contained focusable.
529 */
530 boolean foundFullyContainedFocusable = false;
531
532 int count = focusables.size();
533 for (int i = 0; i < count; i++)
534 {
535 View view = focusables.get(i);
536 int viewTop = view.getTop();
537 int viewBottom = view.getBottom();
538 int viewLeft = view.getLeft();
539 int viewRight = view.getRight();
540
541 if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right)
542 {
543 /*
544 * the focusable is in the target area, it is a candidate for
545 * focusing
546 */
547 final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) &&
548 (left < viewLeft) && (viewRight < right);
549 if (focusCandidate == null)
550 {
551 /* No candidate, take this one */
552 focusCandidate = view;
553 foundFullyContainedFocusable = viewIsFullyContained;
554 }
555 else
556 {
557 final boolean viewIsCloserToVerticalBoundary =
558 (topFocus && viewTop < focusCandidate.getTop()) ||
559 (!topFocus && viewBottom > focusCandidate.getBottom());
560 final boolean viewIsCloserToHorizontalBoundary =
561 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
562 (!leftFocus && viewRight > focusCandidate.getRight());
563 if (foundFullyContainedFocusable)
564 {
565 if (viewIsFullyContained && viewIsCloserToVerticalBoundary &&
566 viewIsCloserToHorizontalBoundary)
567 {
568 /*
569 * We're dealing with only fully contained views, so
570 * it has to be closer to the boundary to beat our
571 * candidate
572 */
573 focusCandidate = view;
574 }
575 }
576 else
577 {
578 if (viewIsFullyContained)
579 {
580 /* Any fully contained view beats a partially contained view */
581 focusCandidate = view;
582 foundFullyContainedFocusable = true;
583 }
584 else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary)
585 {
586 /*
587 * Partially contained view beats another partially
588 * contained view if it's closer
589 */
590 focusCandidate = view;
591 }
592 }
593 }
594 }
595 }
596 return focusCandidate;
597 }
598
611 public boolean fullScroll(int direction, boolean horizontal)
612 {
613 if (!horizontal)
614 {
615 boolean down = direction == View.FOCUS_DOWN;
616 int height = getHeight();
617 mTempRect.top = 0;
618 mTempRect.bottom = height;
619 if (down)
620 {
621 int count = getChildCount();
622 if (count > 0)
623 {
624 View view = getChildAt(count - 1);
625 mTempRect.bottom = view.getBottom();
626 mTempRect.top = mTempRect.bottom - height;
627 }
628 }
629 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0);
630 }
631 else
632 {
633 boolean right = direction == View.FOCUS_DOWN;
634 int width = getWidth();
635 mTempRect.left = 0;
636 mTempRect.right = width;
637 if (right)
638 {
639 int count = getChildCount();
640 if (count > 0)
641 {
642 View view = getChildAt(count - 1);
643 mTempRect.right = view.getBottom();
644 mTempRect.left = mTempRect.right - width;
645 }
646 }
647 return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom);
648 }
649 }
650
664 private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left,
665 int right)
666 {
667 boolean handled = true;
668 int height = getHeight();
669 int containerTop = getScrollY();
670 int containerBottom = containerTop + height;
671 boolean up = directionY == View.FOCUS_UP;
672 int width = getWidth();
673 int containerLeft = getScrollX();
674 int containerRight = containerLeft + width;
675 boolean leftwards = directionX == View.FOCUS_UP;
676 View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right);
677 if (newFocused == null)
678 {
679 newFocused = this;
680 }
681 if ((top >= containerTop && bottom <= containerBottom) ||
682 (left >= containerLeft && right <= containerRight))
683 {
684 handled = false;
685 }
686 else
687 {
688 int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
689 int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
690 doScroll(deltaX, deltaY);
691 }
692 if (newFocused != findFocus() && newFocused.requestFocus(directionY))
693 {
694 mTwoDScrollViewMovedFocus = true;
695 mTwoDScrollViewMovedFocus = false;
696 }
697 return handled;
698 }
699
707 public boolean arrowScroll(int direction, boolean horizontal)
708 {
709 View currentFocused = findFocus();
710 if (currentFocused == this)
711 currentFocused = null;
712 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
713 final int maxJump =
714 horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical();
715
716 if (!horizontal)
717 {
718 if (nextFocused != null)
719 {
720 nextFocused.getDrawingRect(mTempRect);
721 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
722 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
723 doScroll(0, scrollDelta);
724 nextFocused.requestFocus(direction);
725 }
726 else
727 {
728 // no new focus
729 int scrollDelta = maxJump;
730 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
731 {
732 scrollDelta = getScrollY();
733 }
734 else if (direction == View.FOCUS_DOWN)
735 {
736 if (getChildCount() > 0)
737 {
738 int daBottom = getChildAt(0).getBottom();
739 int screenBottom = getScrollY() + getHeight();
740 if (daBottom - screenBottom < maxJump)
741 {
742 scrollDelta = daBottom - screenBottom;
743 }
744 }
745 }
746 if (scrollDelta == 0)
747 {
748 return false;
749 }
750 doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
751 }
752 }
753 else
754 {
755 if (nextFocused != null)
756 {
757 nextFocused.getDrawingRect(mTempRect);
758 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
759 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
760 doScroll(scrollDelta, 0);
761 nextFocused.requestFocus(direction);
762 }
763 else
764 {
765 // no new focus
766 int scrollDelta = maxJump;
767 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta)
768 {
769 scrollDelta = getScrollY();
770 }
771 else if (direction == View.FOCUS_DOWN)
772 {
773 if (getChildCount() > 0)
774 {
775 int daBottom = getChildAt(0).getBottom();
776 int screenBottom = getScrollY() + getHeight();
777 if (daBottom - screenBottom < maxJump)
778 {
779 scrollDelta = daBottom - screenBottom;
780 }
781 }
782 }
783 if (scrollDelta == 0)
784 {
785 return false;
786 }
787 doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
788 }
789 }
790 return true;
791 }
792
798 private void doScroll(int deltaX, int deltaY)
799 {
800 if (deltaX != 0 || deltaY != 0)
801 {
802 smoothScrollBy(deltaX, deltaY);
803 }
804 }
805
812 public final void smoothScrollBy(int dx, int dy)
813 {
814 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
815 if (duration > ANIMATED_SCROLL_GAP)
816 {
817 mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
818 awakenScrollBars(mScroller.getDuration());
819 invalidate();
820 }
821 else
822 {
823 if (!mScroller.isFinished())
824 {
825 mScroller.abortAnimation();
826 }
827 scrollBy(dx, dy);
828 }
829 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
830 }
831
838 public final void smoothScrollTo(int x, int y)
839 {
840 smoothScrollBy(x - getScrollX(), y - getScrollY());
841 }
842
847 @Override protected int computeVerticalScrollRange()
848 {
849 int count = getChildCount();
850 return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
851 }
852
853 @Override protected int computeHorizontalScrollRange()
854 {
855 int count = getChildCount();
856 return count == 0 ? getWidth() : (getChildAt(0)).getRight();
857 }
858
859 @Override
860 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
861 {
862 ViewGroup.LayoutParams lp = child.getLayoutParams();
863 int childWidthMeasureSpec;
864 int childHeightMeasureSpec;
865
866 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
867 getPaddingLeft() + getPaddingRight(), lp.width);
868 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
869
870 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
871 }
872
873 @Override
874 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
875 int parentHeightMeasureSpec, int heightUsed)
876 {
877 final MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
878 final int childWidthMeasureSpec =
879 MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
880 final int childHeightMeasureSpec =
881 MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
882
883 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
884 }
885
886 @Override public void computeScroll()
887 {
888 if (mScroller.computeScrollOffset())
889 {
890 // This is called at drawing time by ViewGroup. We don't want to
891 // re-show the scrollbars at this point, which scrollTo will do,
892 // so we replicate most of scrollTo here.
893 //
894 // It's a little odd to call onScrollChanged from inside the drawing.
895 //
896 // It is, except when you remember that computeScroll() is used to
897 // animate scrolling. So unless we want to defer the onScrollChanged()
898 // until the end of the animated scrolling, we don't really have a
899 // choice here.
900 //
901 // I agree. The alternative, which I think would be worse, is to post
902 // something and tell the subclasses later. This is bad because there
903 // will be a window where mScrollX/Y is different from what the app
904 // thinks it is.
905 //
906 int oldX = getScrollX();
907 int oldY = getScrollY();
908 int x = mScroller.getCurrX();
909 int y = mScroller.getCurrY();
910 if (getChildCount() > 0)
911 {
912 View child = getChildAt(0);
913 scrollTo(
914 clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()),
915 clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(),
916 child.getHeight()));
917 }
918 else
919 {
920 scrollTo(x, y);
921 }
922 if (oldX != getScrollX() || oldY != getScrollY())
923 {
924 onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
925 }
926
927 // Keep on drawing until the animation has finished.
928 postInvalidate();
929 }
930 }
931
937 private void scrollToChild(View child)
938 {
939 child.getDrawingRect(mTempRect);
940 /* Offset from child's local coordinates to TwoDScrollView coordinates */
941 offsetDescendantRectToMyCoords(child, mTempRect);
942 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
943 if (scrollDelta != 0)
944 {
945 scrollBy(0, scrollDelta);
946 }
947 }
948
957 private boolean scrollToChildRect(Rect rect, boolean immediate)
958 {
959 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
960 final boolean scroll = delta != 0;
961 if (scroll)
962 {
963 if (immediate)
964 {
965 scrollBy(0, delta);
966 }
967 else
968 {
969 smoothScrollBy(0, delta);
970 }
971 }
972 return scroll;
973 }
974
984 {
985 if (getChildCount() == 0)
986 return 0;
987 int height = getHeight();
988 int screenTop = getScrollY();
989 int screenBottom = screenTop + height;
990 int fadingEdge = getVerticalFadingEdgeLength();
991 // leave room for top fading edge as long as rect isn't at very top
992 if (rect.top > 0)
993 {
994 screenTop += fadingEdge;
995 }
996
997 // leave room for bottom fading edge as long as rect isn't at very bottom
998 if (rect.bottom < getChildAt(0).getHeight())
999 {
1000 screenBottom -= fadingEdge;
1001 }
1002 int scrollYDelta = 0;
1003 if (rect.bottom > screenBottom && rect.top > screenTop)
1004 {
1005 // need to move down to get it in view: move down just enough so
1006 // that the entire rectangle is in view (or at least the first
1007 // screen size chunk).
1008 if (rect.height() > height)
1009 {
1010 // just enough to get screen size chunk on
1011 scrollYDelta += (rect.top - screenTop);
1012 }
1013 else
1014 {
1015 // get entire rect at bottom of screen
1016 scrollYDelta += (rect.bottom - screenBottom);
1017 }
1018
1019 // make sure we aren't scrolling beyond the end of our content
1020 int bottom = getChildAt(0).getBottom();
1021 int distanceToBottom = bottom - screenBottom;
1022 scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
1023 }
1024 else if (rect.top < screenTop && rect.bottom < screenBottom)
1025 {
1026 // need to move up to get it in view: move up just enough so that
1027 // entire rectangle is in view (or at least the first screen
1028 // size chunk of it).
1029
1030 if (rect.height() > height)
1031 {
1032 // screen size chunk
1033 scrollYDelta -= (screenBottom - rect.bottom);
1034 }
1035 else
1036 {
1037 // entire rect at top
1038 scrollYDelta -= (screenTop - rect.top);
1039 }
1040
1041 // make sure we aren't scrolling any further than the top our content
1042 scrollYDelta = Math.max(scrollYDelta, -getScrollY());
1043 }
1044 return scrollYDelta;
1045 }
1046
1047 @Override public void requestChildFocus(View child, View focused)
1048 {
1049 if (!mTwoDScrollViewMovedFocus)
1050 {
1051 if (!mIsLayoutDirty)
1052 {
1053 scrollToChild(focused);
1054 }
1055 else
1056 {
1057 // The child may not be laid out yet, we can't compute the scroll yet
1058 mChildToScrollTo = focused;
1059 }
1060 }
1061 super.requestChildFocus(child, focused);
1062 }
1063
1071 @Override
1072 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)
1073 {
1074 // convert from forward / backward notation to up / down / left / right
1075 // (ugh).
1076 if (direction == View.FOCUS_FORWARD)
1077 {
1078 direction = View.FOCUS_DOWN;
1079 }
1080 else if (direction == View.FOCUS_BACKWARD)
1081 {
1082 direction = View.FOCUS_UP;
1083 }
1084
1085 final View nextFocus = previouslyFocusedRect == null
1086 ? FocusFinder.getInstance().findNextFocus(this, null, direction)
1087 : FocusFinder.getInstance().findNextFocusFromRect(
1088 this, previouslyFocusedRect, direction);
1089
1090 if (nextFocus == null)
1091 {
1092 return false;
1093 }
1094
1095 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1096 }
1097
1098 @Override
1099 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)
1100 {
1101 // offset into coordinate space of this scroll view
1102 rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
1103 return scrollToChildRect(rectangle, immediate);
1104 }
1105
1106 @Override public void requestLayout()
1107 {
1108 mIsLayoutDirty = true;
1109 super.requestLayout();
1110 }
1111
1112 private int lastCenterContentH = -1;
1113 private int lastCenterTop = 0;
1114
1115 @Override protected void onLayout(boolean changed, int l, int t, int r, int b)
1116 {
1117 super.onLayout(changed, l, t, r, b);
1118 mIsLayoutDirty = false;
1119 // Give a child focus if it needs it
1120 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this))
1121 {
1122 scrollToChild(mChildToScrollTo);
1123 }
1124 mChildToScrollTo = null;
1125
1126 // Center the content area (excluding touch-pointer padding) within the viewport.
1127 // This keeps the RDP surface visually centered regardless of pointer padding changes.
1128 if (getChildCount() > 0)
1129 {
1130 View child = getChildAt(0);
1131 SessionView sv = findViewById(R.id.sessionView);
1132 int ptw = sv != null ? sv.getTouchPointerPaddingWidth() : 0;
1133 int pth = sv != null ? sv.getTouchPointerPaddingHeight() : 0;
1134 int contentW = child.getMeasuredWidth() - ptw;
1135 int contentH = child.getMeasuredHeight() - pth;
1136 int usableW = getWidth() - getPaddingLeft() - getPaddingRight();
1137 int usableH = getHeight() - getPaddingTop() - getPaddingBottom();
1138 int left = getPaddingLeft() + Math.max(0, (usableW - contentW) / 2);
1139 int top;
1140 if (contentH == lastCenterContentH)
1141 {
1142 // viewport-only change (e.g. keyboard): keep position, don't re-center
1143 top = lastCenterTop;
1144 }
1145 else
1146 {
1147 top = getPaddingTop() + Math.max(0, (usableH - contentH) / 2);
1148 lastCenterContentH = contentH;
1149 lastCenterTop = top;
1150 }
1151 child.layout(left, top, left + child.getMeasuredWidth(),
1152 top + child.getMeasuredHeight());
1153 }
1154
1155 // Calling this with the present values causes it to re-clamp them
1156 scrollTo(getScrollX(), getScrollY());
1157 }
1158
1159 @Override protected void onSizeChanged(int w, int h, int oldw, int oldh)
1160 {
1161 super.onSizeChanged(w, h, oldw, oldh);
1162
1163 View currentFocused = findFocus();
1164 if (null == currentFocused || this == currentFocused)
1165 return;
1166
1167 // If the currently-focused view was visible on the screen when the
1168 // screen was at the old height, then scroll the screen to make that
1169 // view visible with the new screen height.
1170 currentFocused.getDrawingRect(mTempRect);
1171 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1172 int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1173 int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1174 doScroll(scrollDeltaX, scrollDeltaY);
1175 }
1176
1180 private boolean isViewDescendantOf(View child, View parent)
1181 {
1182 if (child == parent)
1183 {
1184 return true;
1185 }
1186
1187 final ViewParent theParent = child.getParent();
1188 return (theParent instanceof ViewGroup) && isViewDescendantOf((View)theParent, parent);
1189 }
1190
1198 public void fling(int velocityX, int velocityY)
1199 {
1200 if (getChildCount() > 0)
1201 {
1202 int height = getHeight() - getPaddingBottom() - getPaddingTop();
1203 int bottom = getChildAt(0).getHeight();
1204 int width = getWidth() - getPaddingRight() - getPaddingLeft();
1205 int right = getChildAt(0).getWidth();
1206
1207 mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0,
1208 bottom - height);
1209
1210 final boolean movingDown = velocityY > 0;
1211 final boolean movingRight = velocityX > 0;
1212
1213 View newFocused = findFocusableViewInMyBounds(
1214 movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus());
1215 if (newFocused == null)
1216 {
1217 newFocused = this;
1218 }
1219
1220 if (newFocused != findFocus() &&
1221 newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP))
1222 {
1223 mTwoDScrollViewMovedFocus = true;
1224 mTwoDScrollViewMovedFocus = false;
1225 }
1226
1227 awakenScrollBars(mScroller.getDuration());
1228 invalidate();
1229 }
1230 }
1231
1237 public void scrollTo(int x, int y)
1238 {
1239 // we rely on the fact the View.scrollBy calls scrollTo.
1240 if (getChildCount() > 0)
1241 {
1242 View child = getChildAt(0);
1243 x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
1244 y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
1245 if (x != getScrollX() || y != getScrollY())
1246 {
1247 super.scrollTo(x, y);
1248 }
1249 }
1250 }
1251
1252 private int clamp(int n, int my, int child)
1253 {
1254 if (my >= child || n < 0)
1255 {
1256 /* my >= child is this case:
1257 * |--------------- me ---------------|
1258 * |------ child ------|
1259 * or
1260 * |--------------- me ---------------|
1261 * |------ child ------|
1262 * or
1263 * |--------------- me ---------------|
1264 * |------ child ------|
1265 *
1266 * n < 0 is this case:
1267 * |------ me ------|
1268 * |-------- child --------|
1269 * |-- mScrollX --|
1270 */
1271 return 0;
1272 }
1273 if ((my + n) > child)
1274 {
1275 /* this case:
1276 * |------ me ------|
1277 * |------ child ------|
1278 * |-- mScrollX --|
1279 */
1280 return child - my;
1281 }
1282 return n;
1283 }
1284
1285 public void setScrollViewListener(ScrollView2DListener scrollViewListener)
1286 {
1287 this.scrollView2DListener = scrollViewListener;
1288 }
1289
1290 @Override protected void onScrollChanged(int x, int y, int oldx, int oldy)
1291 {
1292 super.onScrollChanged(x, y, oldx, oldy);
1293 if (scrollView2DListener != null)
1294 {
1295 scrollView2DListener.onScrollChanged(this, x, y, oldx, oldy);
1296 }
1297 }
1298
1299 // interface to receive notifications when the view is scrolled
1300 public interface ScrollView2DListener {
1301 void onScrollChanged(ScrollView2D scrollView, int x, int y, int oldx, int oldy);
1302 }
1303}
boolean arrowScroll(int direction, boolean horizontal)
boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)
boolean fullScroll(int direction, boolean horizontal)