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