]> matita.cs.unibo.it Git - logicplayer.git/blob - mainActivity/src/com/example/furt/myapplication/TwoDScrollView.java
The applet.
[logicplayer.git] / mainActivity / src / com / example / furt / myapplication / TwoDScrollView.java
1 package com.example.furt.myapplication;
2
3 import android.content.Context;
4 import android.graphics.Rect;
5 import android.util.AttributeSet;
6 import android.view.FocusFinder;
7 import android.view.KeyEvent;
8 import android.view.MotionEvent;
9 import android.view.VelocityTracker;
10 import android.view.View;
11 import android.view.ViewConfiguration;
12 import android.view.ViewGroup;
13 import android.view.ViewParent;
14 import android.view.animation.AnimationUtils;
15 import android.widget.FrameLayout;
16 import android.widget.Scroller;
17
18 import java.util.List;
19
20
21 /**
22  * Copyright (C) 2006 The Android Open Source Project
23  *
24  * Licensed under the Apache License, Version 2.0 (the "License");
25  * you may not use this file except in compliance with the License.
26  * You may obtain a copy of the License at
27  *
28  *      http://www.apache.org/licenses/LICENSE-2.0
29  *
30  * Unless required by applicable law or agreed to in writing, software
31  * distributed under the License is distributed on an "AS IS" BASIS,
32  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33  * See the License for the specific language governing permissions and
34  * limitations under the License.
35  *
36 *
37  * Revised 5/19/2010 by GORGES
38  * Now supports two-dimensional view scrolling
39  * http://GORGES.us
40  */
41
42 /**
43  * Layout container for a view hierarchy that can be scrolled by the user,
44  * allowing it to be larger than the physical display.  A TwoDScrollView
45  * is a {@link android.widget.FrameLayout}, meaning you should place one child in it
46  * containing the entire contents to scroll; this child may itself be a layout
47  * manager with a complex hierarchy of objects.  A child that is often used
48  * is a {@link android.widget.LinearLayout} in a vertical orientation, presenting a vertical
49  * array of top-level items that the user can scroll through.
50  *
51  * <p>The {@link android.widget.TextView} class also
52  * takes care of its own scrolling, so does not require a TwoDScrollView, but
53  * using the two together is possible to achieve the effect of a text view
54  * within a larger container.
55  */
56
57 public class TwoDScrollView extends FrameLayout {
58     static final int ANIMATED_SCROLL_GAP = 250;
59     static final float MAX_SCROLL_FACTOR = 0.5f;
60
61     private long mLastScroll;
62
63     private final Rect mTempRect = new Rect();
64     private Scroller mScroller;
65
66     /*
67      * Flag to indicate that we are moving focus ourselves. This is so the
68      * code that watches for focus changes initiated outside this TwoDScrollView
69      * knows that it does not have to do anything.
70      */
71     private boolean mTwoDScrollViewMovedFocus;
72
73     /*
74      * Position of the last motion event.
75      */
76     private float mLastMotionY;
77     private float mLastMotionX;
78
79     /*
80      * True when the layout has changed but the traversal has not come through yet.
81      * Ideally the view hierarchy would keep track of this for us.
82      */
83     private boolean mIsLayoutDirty = true;
84
85     /*
86      * The child to give focus to in the event that a child has requested focus while the
87      * layout is dirty. This prevents the scroll from being wrong if the child has not been
88      * laid out before requesting focus.
89      */
90     private View mChildToScrollTo = null;
91
92     /*
93      * True if the user is currently dragging this TwoDScrollView around. This is
94      * not the same as 'is being flinged', which can be checked by
95      * mScroller.isFinished() (flinging begins when the user lifts his finger).
96      */
97     private boolean mIsBeingDragged = false;
98
99     /*
100      * Determines speed during touch scrolling
101      */
102     private VelocityTracker mVelocityTracker;
103
104     /*
105      * Whether arrow scrolling is animated.
106      */
107     private int mTouchSlop;
108     private int mMinimumVelocity;
109     private int mMaximumVelocity;
110
111     public TwoDScrollView(Context context) {
112         super(context);
113         initTwoDScrollView();
114     }
115
116     public TwoDScrollView(Context context, AttributeSet attrs) {
117         super(context, attrs);
118         initTwoDScrollView();
119     }
120
121     public TwoDScrollView(Context context, AttributeSet attrs, int defStyle) {
122         super(context, attrs, defStyle);
123         initTwoDScrollView();
124     }
125
126     @Override
127     protected float getTopFadingEdgeStrength() {
128         if (getChildCount() == 0) {
129             return 0.0f;
130         }
131         final int length = getVerticalFadingEdgeLength();
132         if (getScrollY() < length) {
133             return getScrollY() / (float) length;
134         }
135         return 1.0f;
136     }
137
138     @Override
139     protected float getBottomFadingEdgeStrength() {
140         if (getChildCount() == 0) {
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             return span / (float) length;
148         }
149         return 1.0f;
150     }
151
152     @Override
153     protected float getLeftFadingEdgeStrength() {
154         if (getChildCount() == 0) {
155             return 0.0f;
156         }
157         final int length = getHorizontalFadingEdgeLength();
158         if (getScrollX() < length) {
159             return getScrollX() / (float) length;
160         }
161         return 1.0f;
162     }
163
164     @Override
165     protected float getRightFadingEdgeStrength() {
166         if (getChildCount() == 0) {
167             return 0.0f;
168         }
169         final int length = getHorizontalFadingEdgeLength();
170         final int rightEdge = getWidth() - getPaddingRight();
171         final int span = getChildAt(0).getRight() - getScrollX() - rightEdge;
172         if (span < length) {
173             return span / (float) length;
174         }
175         return 1.0f;
176     }
177
178     /**
179      * @return The maximum amount this scroll view will scroll in response to
180      *   an arrow event.
181      */
182     public int getMaxScrollAmountVertical() {
183         return (int) (MAX_SCROLL_FACTOR * getHeight());
184     }
185     public int getMaxScrollAmountHorizontal() {
186         return (int) (MAX_SCROLL_FACTOR * getWidth());
187     }
188
189     private void initTwoDScrollView() {
190         mScroller = new Scroller(getContext());
191         setFocusable(true);
192         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
193         setWillNotDraw(false);
194         final ViewConfiguration configuration = ViewConfiguration.get(getContext());
195         mTouchSlop = configuration.getScaledTouchSlop();
196         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
197         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
198     }
199
200     @Override
201     public void addView(View child) {
202         if (getChildCount() > 0) {
203             throw new IllegalStateException("TwoDScrollView can host only one direct child");
204         }
205         super.addView(child);
206     }
207
208     @Override
209     public void addView(View child, int index) {
210         if (getChildCount() > 0) {
211             throw new IllegalStateException("TwoDScrollView can host only one direct child");
212         }
213         super.addView(child, index);
214     }
215
216     @Override
217     public void addView(View child, ViewGroup.LayoutParams params) {
218         if (getChildCount() > 0) {
219             throw new IllegalStateException("TwoDScrollView can host only one direct child");
220         }
221         super.addView(child, params);
222     }
223
224     @Override
225     public void addView(View child, int index, ViewGroup.LayoutParams params) {
226         if (getChildCount() > 0) {
227             throw new IllegalStateException("TwoDScrollView can host only one direct child");
228         }
229         super.addView(child, index, params);
230     }
231
232     /**
233      * @return Returns true this TwoDScrollView can be scrolled
234      */
235     private boolean canScroll() {
236         View child = getChildAt(0);
237         if (child != null) {
238             int childHeight = child.getHeight();
239             int childWidth = child.getWidth();
240             return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) ||
241                     (getWidth() < childWidth + getPaddingLeft() + getPaddingRight());
242         }
243         return false;
244     }
245
246     @Override
247     public boolean dispatchKeyEvent(KeyEvent event) {
248         // Let the focused view and/or our descendants get the key first
249         boolean handled = super.dispatchKeyEvent(event);
250         if (handled) {
251             return true;
252         }
253         return executeKeyEvent(event);
254     }
255
256     /**
257      * You can call this function yourself to have the scroll view perform
258      * scrolling from a key event, just as if the event had been dispatched to
259      * it by the view hierarchy.
260      *
261      * @param event The key event to execute.
262      * @return Return true if the event was handled, else false.
263      */
264     public boolean executeKeyEvent(KeyEvent event) {
265         mTempRect.setEmpty();
266         if (!canScroll()) {
267             if (isFocused()) {
268                 View currentFocused = findFocus();
269                 if (currentFocused == this) currentFocused = null;
270                 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN);
271                 return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN);
272             }
273             return false;
274         }
275         boolean handled = false;
276         if (event.getAction() == KeyEvent.ACTION_DOWN) {
277             switch (event.getKeyCode()) {
278                 case KeyEvent.KEYCODE_DPAD_UP:
279                     if (!event.isAltPressed()) {
280                         handled = arrowScroll(View.FOCUS_UP, false);
281                     } else {
282                         handled = fullScroll(View.FOCUS_UP, false);
283                     }
284                     break;
285                 case KeyEvent.KEYCODE_DPAD_DOWN:
286                     if (!event.isAltPressed()) {
287                         handled = arrowScroll(View.FOCUS_DOWN, false);
288                     } else {
289                         handled = fullScroll(View.FOCUS_DOWN, false);
290                     }
291                     break;
292                 case KeyEvent.KEYCODE_DPAD_LEFT:
293                     if (!event.isAltPressed()) {
294                         handled = arrowScroll(View.FOCUS_LEFT, true);
295                     } else {
296                         handled = fullScroll(View.FOCUS_LEFT, true);
297                     }
298                     break;
299                 case KeyEvent.KEYCODE_DPAD_RIGHT:
300                     if (!event.isAltPressed()) {
301                         handled = arrowScroll(View.FOCUS_RIGHT, true);
302                     } else {
303                         handled = fullScroll(View.FOCUS_RIGHT, true);
304                     }
305                     break;
306             }
307         }
308         return handled;
309     }
310
311     @Override
312     public boolean onInterceptTouchEvent(MotionEvent ev) {
313    /*
314    * This method JUST determines whether we want to intercept the motion.
315    * If we return true, onMotionEvent will be called and we do the actual
316    * scrolling there.
317    *
318    * Shortcut the most recurring case: the user is in the dragging
319    * state and he is moving his finger.  We want to intercept this
320    * motion.
321    */
322         final int action = ev.getAction();
323         if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
324             return true;
325         }
326         if (!canScroll()) {
327             mIsBeingDragged = false;
328             return false;
329         }
330         final float y = ev.getY();
331         final float x = ev.getX();
332         switch (action) {
333             case MotionEvent.ACTION_MOVE:
334        /*
335        * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
336        * whether the user has moved far enough from his original down touch.
337        */
338        /*
339        * Locally do absolute value. mLastMotionY is set to the y value
340        * of the down event.
341        */
342                 final int yDiff = (int) Math.abs(y - mLastMotionY);
343                 final int xDiff = (int) Math.abs(x - mLastMotionX);
344                 if (yDiff > mTouchSlop || xDiff > mTouchSlop) {
345                     mIsBeingDragged = true;
346                 }
347                 break;
348
349             case MotionEvent.ACTION_DOWN:
350        /* Remember location of down touch */
351                 mLastMotionY = y;
352                 mLastMotionX = x;
353
354        /*
355        * If being flinged and user touches the screen, initiate drag;
356        * otherwise don't.  mScroller.isFinished should be false when
357        * being flinged.
358        */
359                 mIsBeingDragged = !mScroller.isFinished();
360                 break;
361
362             case MotionEvent.ACTION_CANCEL:
363             case MotionEvent.ACTION_UP:
364        /* Release the drag */
365                 mIsBeingDragged = false;
366                 break;
367         }
368
369    /*
370    * The only time we want to intercept motion events is if we are in the
371    * drag mode.
372    */
373         return mIsBeingDragged;
374     }
375
376     @Override
377     public boolean onTouchEvent(MotionEvent ev) {
378
379         if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
380             // Don't handle edge touches immediately -- they may actually belong to one of our
381             // descendants.
382             return false;
383         }
384
385         if (!canScroll()) {
386             return false;
387         }
388
389         if (mVelocityTracker == null) {
390             mVelocityTracker = VelocityTracker.obtain();
391         }
392         mVelocityTracker.addMovement(ev);
393
394         final int action = ev.getAction();
395         final float y = ev.getY();
396         final float x = ev.getX();
397
398         switch (action) {
399             case MotionEvent.ACTION_DOWN:
400        /*
401        * If being flinged and user touches, stop the fling. isFinished
402        * will be false if being flinged.
403        */
404                 if (!mScroller.isFinished()) {
405                     mScroller.abortAnimation();
406                 }
407
408                 // Remember where the motion event started
409                 mLastMotionY = y;
410                 mLastMotionX = x;
411                 break;
412             case MotionEvent.ACTION_MOVE:
413                 // Scroll to follow the motion event
414                 int deltaX = (int) (mLastMotionX - x);
415                 int deltaY = (int) (mLastMotionY - y);
416                 mLastMotionX = x;
417                 mLastMotionY = y;
418
419                 if (deltaX < 0) {
420                     if (getScrollX() < 0) {
421                         deltaX = 0;
422                     }
423                 } else if (deltaX > 0) {
424                     final int rightEdge = getWidth() - getPaddingRight();
425                     final int availableToScroll = getChildAt(0).getRight() - getScrollX() - rightEdge;
426                     if (availableToScroll > 0) {
427                         deltaX = Math.min(availableToScroll, deltaX);
428                     } else {
429                         deltaX = 0;
430                     }
431                 }
432                 if (deltaY < 0) {
433                     if (getScrollY() < 0) {
434                         deltaY = 0;
435                     }
436                 } else if (deltaY > 0) {
437                     final int bottomEdge = getHeight() - getPaddingBottom();
438                     final int availableToScroll = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
439                     if (availableToScroll > 0) {
440                         deltaY = Math.min(availableToScroll, deltaY);
441                     } else {
442                         deltaY = 0;
443                     }
444                 }
445                 if (deltaY != 0 || deltaX != 0)
446                     scrollBy(deltaX, deltaY);
447                 break;
448             case MotionEvent.ACTION_UP:
449                 final VelocityTracker velocityTracker = mVelocityTracker;
450                 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
451                 int initialXVelocity = (int) velocityTracker.getXVelocity();
452                 int initialYVelocity = (int) velocityTracker.getYVelocity();
453                 if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) && getChildCount() > 0) {
454                     fling(-initialXVelocity, -initialYVelocity);
455                 }
456                 if (mVelocityTracker != null) {
457                     mVelocityTracker.recycle();
458                     mVelocityTracker = null;
459                 }
460         }
461         return true;
462     }
463
464     /**
465      * Finds the next focusable component that fits in this View's bounds
466      * (excluding fading edges) pretending that this View's top is located at
467      * the parameter top.
468      *
469      * @param topFocus           look for a candidate is the one at the top of the bounds
470      *                           if topFocus is true, or at the bottom of the bounds if topFocus is
471      *                           false
472      * @param top                the top offset of the bounds in which a focusable must be
473      *                           found (the fading edge is assumed to start at this position)
474      * @param preferredFocusable the View that has highest priority and will be
475      *                           returned if it is within my bounds (null is valid)
476      * @return the next focusable component in the bounds or null if none can be
477      *         found
478      */
479     private View findFocusableViewInMyBounds(final boolean topFocus, final int top, final boolean leftFocus, final int left, View preferredFocusable) {
480    /*
481    * The fading edge's transparent side should be considered for focus
482    * since it's mostly visible, so we divide the actual fading edge length
483    * by 2.
484    */
485         final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2;
486         final int topWithoutFadingEdge = top + verticalFadingEdgeLength;
487         final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength;
488         final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
489         final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength;
490         final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength;
491
492         if ((preferredFocusable != null)
493                 && (preferredFocusable.getTop() < bottomWithoutFadingEdge)
494                 && (preferredFocusable.getBottom() > topWithoutFadingEdge)
495                 && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
496                 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
497             return preferredFocusable;
498         }
499         return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge, leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
500     }
501
502     /**
503      * Finds the next focusable component that fits in the specified bounds.
504      * </p>
505      *
506      * @param topFocus look for a candidate is the one at the top of the bounds
507      *                 if topFocus is true, or at the bottom of the bounds if topFocus is
508      *                 false
509      * @param top      the top offset of the bounds in which a focusable must be
510      *                 found
511      * @param bottom   the bottom offset of the bounds in which a focusable must
512      *                 be found
513      * @return the next focusable component in the bounds or null if none can
514      *         be found
515      */
516     private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus, int left, int right) {
517         List<View> focusables = getFocusables(View.FOCUS_FORWARD);
518         View focusCandidate = null;
519
520    /*
521    * A fully contained focusable is one where its top is below the bound's
522    * top, and its bottom is above the bound's bottom. A partially
523    * contained focusable is one where some part of it is within the
524    * bounds, but it also has some part that is not within bounds.  A fully contained
525    * focusable is preferred to a partially contained focusable.
526    */
527         boolean foundFullyContainedFocusable = false;
528
529         int count = focusables.size();
530         for (int i = 0; i < count; i++) {
531             View view = focusables.get(i);
532             int viewTop = view.getTop();
533             int viewBottom = view.getBottom();
534             int viewLeft = view.getLeft();
535             int viewRight = view.getRight();
536
537             if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right) {
538        /*
539        * the focusable is in the target area, it is a candidate for
540        * focusing
541        */
542                 final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) && (left < viewLeft) && (viewRight < right);
543                 if (focusCandidate == null) {
544          /* No candidate, take this one */
545                     focusCandidate = view;
546                     foundFullyContainedFocusable = viewIsFullyContained;
547                 } else {
548                     final boolean viewIsCloserToVerticalBoundary =
549                             (topFocus && viewTop < focusCandidate.getTop()) ||
550                                     (!topFocus && viewBottom > focusCandidate.getBottom());
551                     final boolean viewIsCloserToHorizontalBoundary =
552                             (leftFocus && viewLeft < focusCandidate.getLeft()) ||
553                                     (!leftFocus && viewRight > focusCandidate.getRight());
554                     if (foundFullyContainedFocusable) {
555                         if (viewIsFullyContained && viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) {
556              /*
557               * We're dealing with only fully contained views, so
558               * it has to be closer to the boundary to beat our
559               * candidate
560               */
561                             focusCandidate = view;
562                         }
563                     } else {
564                         if (viewIsFullyContained) {
565              /* Any fully contained view beats a partially contained view */
566                             focusCandidate = view;
567                             foundFullyContainedFocusable = true;
568                         } else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) {
569              /*
570               * Partially contained view beats another partially
571               * contained view if it's closer
572               */
573                             focusCandidate = view;
574                         }
575                     }
576                 }
577             }
578         }
579         return focusCandidate;
580     }
581
582     /**
583      * <p>Handles scrolling in response to a "home/end" shortcut press. This
584      * method will scroll the view to the top or bottom and give the focus
585      * to the topmost/bottommost component in the new visible area. If no
586      * component is a good candidate for focus, this scrollview reclaims the
587      * focus.</p>
588      *
589      * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
590      *                  to go the top of the view or
591      *                  {@link android.view.View#FOCUS_DOWN} to go the bottom
592      * @return true if the key event is consumed by this method, false otherwise
593      */
594     public boolean fullScroll(int direction, boolean horizontal) {
595         if (!horizontal) {
596             boolean down = direction == View.FOCUS_DOWN;
597             int height = getHeight();
598             mTempRect.top = 0;
599             mTempRect.bottom = height;
600             if (down) {
601                 int count = getChildCount();
602                 if (count > 0) {
603                     View view = getChildAt(count - 1);
604                     mTempRect.bottom = view.getBottom();
605                     mTempRect.top = mTempRect.bottom - height;
606                 }
607             }
608             return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0);
609         } else {
610             boolean right = direction == View.FOCUS_DOWN;
611             int width = getWidth();
612             mTempRect.left = 0;
613             mTempRect.right = width;
614             if (right) {
615                 int count = getChildCount();
616                 if (count > 0) {
617                     View view = getChildAt(count - 1);
618                     mTempRect.right = view.getBottom();
619                     mTempRect.left = mTempRect.right - width;
620                 }
621             }
622             return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom);
623         }
624     }
625
626     /**
627      * <p>Scrolls the view to make the area defined by <code>top</code> and
628      * <code>bottom</code> visible. This method attempts to give the focus
629      * to a component visible in this area. If no component can be focused in
630      * the new visible area, the focus is reclaimed by this scrollview.</p>
631      *
632      * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
633      *                  to go upward
634      *                  {@link android.view.View#FOCUS_DOWN} to downward
635      * @param top       the top offset of the new area to be made visible
636      * @param bottom    the bottom offset of the new area to be made visible
637      * @return true if the key event is consumed by this method, false otherwise
638      */
639     private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left, int right) {
640         boolean handled = true;
641         int height = getHeight();
642         int containerTop = getScrollY();
643         int containerBottom = containerTop + height;
644         boolean up = directionY == View.FOCUS_UP;
645         int width = getWidth();
646         int containerLeft = getScrollX();
647         int containerRight = containerLeft + width;
648         boolean leftwards = directionX == View.FOCUS_UP;
649         View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right);
650         if (newFocused == null) {
651             newFocused = this;
652         }
653         if ((top >= containerTop && bottom <= containerBottom) || (left >= containerLeft && right <= containerRight)) {
654             handled = false;
655         } else {
656             int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
657             int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
658             doScroll(deltaX, deltaY);
659         }
660         if (newFocused != findFocus() && newFocused.requestFocus(directionY)) {
661             mTwoDScrollViewMovedFocus = true;
662             mTwoDScrollViewMovedFocus = false;
663         }
664         return handled;
665     }
666
667     /**
668      * Handle scrolling in response to an up or down arrow click.
669      *
670      * @param direction The direction corresponding to the arrow key that was
671      *                  pressed
672      * @return True if we consumed the event, false otherwise
673      */
674     public boolean arrowScroll(int direction, boolean horizontal) {
675         View currentFocused = findFocus();
676         if (currentFocused == this) currentFocused = null;
677         View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
678         final int maxJump = horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical();
679
680         if (!horizontal) {
681             if (nextFocused != null) {
682                 nextFocused.getDrawingRect(mTempRect);
683                 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
684                 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
685                 doScroll(0, scrollDelta);
686                 nextFocused.requestFocus(direction);
687             } else {
688                 // no new focus
689                 int scrollDelta = maxJump;
690                 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
691                     scrollDelta = getScrollY();
692                 } else if (direction == View.FOCUS_DOWN) {
693                     if (getChildCount() > 0) {
694                         int daBottom = getChildAt(0).getBottom();
695                         int screenBottom = getScrollY() + getHeight();
696                         if (daBottom - screenBottom < maxJump) {
697                             scrollDelta = daBottom - screenBottom;
698                         }
699                     }
700                 }
701                 if (scrollDelta == 0) {
702                     return false;
703                 }
704                 doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
705             }
706         } else {
707             if (nextFocused != null) {
708                 nextFocused.getDrawingRect(mTempRect);
709                 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
710                 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
711                 doScroll(scrollDelta, 0);
712                 nextFocused.requestFocus(direction);
713             } else {
714                 // no new focus
715                 int scrollDelta = maxJump;
716                 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
717                     scrollDelta = getScrollY();
718                 } else if (direction == View.FOCUS_DOWN) {
719                     if (getChildCount() > 0) {
720                         int daBottom = getChildAt(0).getBottom();
721                         int screenBottom = getScrollY() + getHeight();
722                         if (daBottom - screenBottom < maxJump) {
723                             scrollDelta = daBottom - screenBottom;
724                         }
725                     }
726                 }
727                 if (scrollDelta == 0) {
728                     return false;
729                 }
730                 doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
731             }
732         }
733         return true;
734     }
735
736     /**
737      * Smooth scroll by a Y delta
738      *
739      * @param delta the number of pixels to scroll by on the Y axis
740      */
741     private void doScroll(int deltaX, int deltaY) {
742         if (deltaX != 0 || deltaY != 0) {
743             smoothScrollBy(deltaX, deltaY);
744         }
745     }
746
747     /**
748      * Like {@link android.view.View#scrollBy}, but scroll smoothly instead of immediately.
749      *
750      * @param dx the number of pixels to scroll by on the X axis
751      * @param dy the number of pixels to scroll by on the Y axis
752      */
753     public final void smoothScrollBy(int dx, int dy) {
754         long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
755         if (duration > ANIMATED_SCROLL_GAP) {
756             mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
757             awakenScrollBars(mScroller.getDuration());
758             invalidate();
759         } else {
760             if (!mScroller.isFinished()) {
761                 mScroller.abortAnimation();
762             }
763             scrollBy(dx, dy);
764         }
765         mLastScroll = AnimationUtils.currentAnimationTimeMillis();
766     }
767
768     /**
769      * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
770      *
771      * @param x the position where to scroll on the X axis
772      * @param y the position where to scroll on the Y axis
773      */
774     public final void smoothScrollTo(int x, int y) {
775         smoothScrollBy(x - getScrollX(), y - getScrollY());
776     }
777
778     /**
779      * <p>The scroll range of a scroll view is the overall height of all of its
780      * children.</p>
781      */
782     @Override
783     protected int computeVerticalScrollRange() {
784         int count = getChildCount();
785         return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
786     }
787     @Override
788     protected int computeHorizontalScrollRange() {
789         int count = getChildCount();
790         return count == 0 ? getWidth() : (getChildAt(0)).getRight();
791     }
792
793     @Override
794     protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
795         ViewGroup.LayoutParams lp = child.getLayoutParams();
796         int childWidthMeasureSpec;
797         int childHeightMeasureSpec;
798
799         childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width);
800         childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
801
802         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
803     }
804
805     @Override
806     protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
807         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
808         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
809                 getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
810         final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
811
812         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
813     }
814
815     @Override
816     public void computeScroll() {
817         if (mScroller.computeScrollOffset()) {
818             // This is called at drawing time by ViewGroup.  We don't want to
819             // re-show the scrollbars at this point, which scrollTo will do,
820             // so we replicate most of scrollTo here.
821             //
822             //         It's a little odd to call onScrollChanged from inside the drawing.
823             //
824             //         It is, except when you remember that computeScroll() is used to
825             //         animate scrolling. So unless we want to defer the onScrollChanged()
826             //         until the end of the animated scrolling, we don't really have a
827             //         choice here.
828             //
829             //         I agree.  The alternative, which I think would be worse, is to post
830             //         something and tell the subclasses later.  This is bad because there
831             //         will be a window where mScrollX/Y is different from what the app
832             //         thinks it is.
833             //
834             int oldX = getScrollX();
835             int oldY = getScrollY();
836             int x = mScroller.getCurrX();
837             int y = mScroller.getCurrY();
838             if (getChildCount() > 0) {
839                 View child = getChildAt(0);
840                 scrollTo(clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()),
841                         clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()));
842             } else {
843                 scrollTo(x, y);
844             }
845             if (oldX != getScrollX() || oldY != getScrollY()) {
846                 onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
847             }
848
849             // Keep on drawing until the animation has finished.
850             postInvalidate();
851         }
852     }
853
854     /**
855      * Scrolls the view to the given child.
856      *
857      * @param child the View to scroll to
858      */
859     private void scrollToChild(View child) {
860         child.getDrawingRect(mTempRect);
861    /* Offset from child's local coordinates to TwoDScrollView coordinates */
862         offsetDescendantRectToMyCoords(child, mTempRect);
863         int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
864         if (scrollDelta != 0) {
865             scrollBy(0, scrollDelta);
866         }
867     }
868
869     /**
870      * If rect is off screen, scroll just enough to get it (or at least the
871      * first screen size chunk of it) on screen.
872      *
873      * @param rect      The rectangle.
874      * @param immediate True to scroll immediately without animation
875      * @return true if scrolling was performed
876      */
877     private boolean scrollToChildRect(Rect rect, boolean immediate) {
878         final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
879         final boolean scroll = delta != 0;
880         if (scroll) {
881             if (immediate) {
882                 scrollBy(0, delta);
883             } else {
884                 smoothScrollBy(0, delta);
885             }
886         }
887         return scroll;
888     }
889
890     /**
891      * Compute the amount to scroll in the Y direction in order to get
892      * a rectangle completely on the screen (or, if taller than the screen,
893      * at least the first screen size chunk of it).
894      *
895      * @param rect The rect.
896      * @return The scroll delta.
897      */
898     protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
899         if (getChildCount() == 0) return 0;
900         int height = getHeight();
901         int screenTop = getScrollY();
902         int screenBottom = screenTop + height;
903         int fadingEdge = getVerticalFadingEdgeLength();
904         // leave room for top fading edge as long as rect isn't at very top
905         if (rect.top > 0) {
906             screenTop += fadingEdge;
907         }
908
909         // leave room for bottom fading edge as long as rect isn't at very bottom
910         if (rect.bottom < getChildAt(0).getHeight()) {
911             screenBottom -= fadingEdge;
912         }
913         int scrollYDelta = 0;
914         if (rect.bottom > screenBottom && rect.top > screenTop) {
915             // need to move down to get it in view: move down just enough so
916             // that the entire rectangle is in view (or at least the first
917             // screen size chunk).
918             if (rect.height() > height) {
919                 // just enough to get screen size chunk on
920                 scrollYDelta += (rect.top - screenTop);
921             } else {
922                 // get entire rect at bottom of screen
923                 scrollYDelta += (rect.bottom - screenBottom);
924             }
925
926             // make sure we aren't scrolling beyond the end of our content
927             int bottom = getChildAt(0).getBottom();
928             int distanceToBottom = bottom - screenBottom;
929             scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
930
931         } else if (rect.top < screenTop && rect.bottom < screenBottom) {
932             // need to move up to get it in view: move up just enough so that
933             // entire rectangle is in view (or at least the first screen
934             // size chunk of it).
935
936             if (rect.height() > height) {
937                 // screen size chunk
938                 scrollYDelta -= (screenBottom - rect.bottom);
939             } else {
940                 // entire rect at top
941                 scrollYDelta -= (screenTop - rect.top);
942             }
943
944             // make sure we aren't scrolling any further than the top our content
945             scrollYDelta = Math.max(scrollYDelta, -getScrollY());
946         }
947         return scrollYDelta;
948     }
949
950     @Override
951     public void requestChildFocus(View child, View focused) {
952         if (!mTwoDScrollViewMovedFocus) {
953             if (!mIsLayoutDirty) {
954                 scrollToChild(focused);
955             } else {
956                 // The child may not be laid out yet, we can't compute the scroll yet
957                 mChildToScrollTo = focused;
958             }
959         }
960         super.requestChildFocus(child, focused);
961     }
962
963     /**
964      * When looking for focus in children of a scroll view, need to be a little
965      * more careful not to give focus to something that is scrolled off screen.
966      *
967      * This is more expensive than the default {@link android.view.ViewGroup}
968      * implementation, otherwise this behavior might have been made the default.
969      */
970     @Override
971     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
972         // convert from forward / backward notation to up / down / left / right
973         // (ugh).
974         if (direction == View.FOCUS_FORWARD) {
975             direction = View.FOCUS_DOWN;
976         } else if (direction == View.FOCUS_BACKWARD) {
977             direction = View.FOCUS_UP;
978         }
979
980         final View nextFocus = previouslyFocusedRect == null ?
981                 FocusFinder.getInstance().findNextFocus(this, null, direction) :
982                 FocusFinder.getInstance().findNextFocusFromRect(this,
983                         previouslyFocusedRect, direction);
984
985         if (nextFocus == null) {
986             return false;
987         }
988
989         return nextFocus.requestFocus(direction, previouslyFocusedRect);
990     }
991
992     @Override
993     public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
994         // offset into coordinate space of this scroll view
995         rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
996         return scrollToChildRect(rectangle, immediate);
997     }
998
999     @Override
1000     public void requestLayout() {
1001         mIsLayoutDirty = true;
1002         super.requestLayout();
1003     }
1004
1005     @Override
1006     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1007         super.onLayout(changed, l, t, r, b);
1008         mIsLayoutDirty = false;
1009         // Give a child focus if it needs it
1010         if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
1011             scrollToChild(mChildToScrollTo);
1012         }
1013         mChildToScrollTo = null;
1014
1015         // Calling this with the present values causes it to re-clam them
1016         scrollTo(getScrollX(), getScrollY());
1017     }
1018
1019     @Override
1020     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1021         super.onSizeChanged(w, h, oldw, oldh);
1022
1023         View currentFocused = findFocus();
1024         if (null == currentFocused || this == currentFocused)
1025             return;
1026
1027         // If the currently-focused view was visible on the screen when the
1028         // screen was at the old height, then scroll the screen to make that
1029         // view visible with the new screen height.
1030         currentFocused.getDrawingRect(mTempRect);
1031         offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1032         int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1033         int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1034         doScroll(scrollDeltaX, scrollDeltaY);
1035     }
1036
1037     /**
1038      * Return true if child is an descendant of parent, (or equal to the parent).
1039      */
1040     private boolean isViewDescendantOf(View child, View parent) {
1041         if (child == parent) {
1042             return true;
1043         }
1044
1045         final ViewParent theParent = child.getParent();
1046         return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1047     }
1048
1049     /**
1050      * Fling the scroll view
1051      *
1052      * @param velocityY The initial velocity in the Y direction. Positive
1053      *                  numbers mean that the finger/curor is moving down the screen,
1054      *                  which means we want to scroll towards the top.
1055      */
1056     public void fling(int velocityX, int velocityY) {
1057         if (getChildCount() > 0) {
1058             int height = getHeight() - getPaddingBottom() - getPaddingTop();
1059             int bottom = getChildAt(0).getHeight();
1060             int width = getWidth() - getPaddingRight() - getPaddingLeft();
1061             int right = getChildAt(0).getWidth();
1062
1063             mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0, bottom - height);
1064
1065             final boolean movingDown = velocityY > 0;
1066             final boolean movingRight = velocityX > 0;
1067
1068             View newFocused = findFocusableViewInMyBounds(movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus());
1069             if (newFocused == null) {
1070                 newFocused = this;
1071             }
1072
1073             if (newFocused != findFocus() && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) {
1074                 mTwoDScrollViewMovedFocus = true;
1075                 mTwoDScrollViewMovedFocus = false;
1076             }
1077
1078             awakenScrollBars(mScroller.getDuration());
1079             invalidate();
1080         }
1081     }
1082
1083     /**
1084      * {@inheritDoc}
1085      *
1086      * <p>This version also clamps the scrolling to the bounds of our child.
1087      */
1088     public void scrollTo(int x, int y) {
1089         // we rely on the fact the View.scrollBy calls scrollTo.
1090         if (getChildCount() > 0) {
1091             View child = getChildAt(0);
1092             x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
1093             y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
1094             if (x != getScrollX() || y != getScrollY()) {
1095                 super.scrollTo(x, y);
1096             }
1097         }
1098     }
1099
1100     private int clamp(int n, int my, int child) {
1101         if (my >= child || n < 0) {
1102      /* my >= child is this case:
1103       *                    |--------------- me ---------------|
1104       *     |------ child ------|
1105       * or
1106       *     |--------------- me ---------------|
1107       *            |------ child ------|
1108       * or
1109       *     |--------------- me ---------------|
1110       *                                  |------ child ------|
1111       *
1112       * n < 0 is this case:
1113       *     |------ me ------|
1114       *                    |-------- child --------|
1115       *     |-- mScrollX --|
1116       */
1117             return 0;
1118         }
1119         if ((my+n) > child) {
1120      /* this case:
1121       *                    |------ me ------|
1122       *     |------ child ------|
1123       *     |-- mScrollX --|
1124       */
1125             return child-my;
1126         }
1127         return n;
1128     }
1129 }