1 /*
2  * Copyright (C) 2014 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 package com.android.tv.settings.widget;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.view.View;
22 import android.view.animation.DecelerateInterpolator;
23 import android.view.animation.LinearInterpolator;
24 import android.widget.Scroller;
25 
26 /**
27  * Maintains a Scroller object and two axis scrolling information
28  */
29 public class ScrollController {
30     /**
31      * try to keep focused view kept in middle of viewport, focus move to the side of viewport when
32      * scroll to the beginning or end, this will make sure you won't see blank space in viewport
33      * {@link Axis.ItemWindow#setCount(int)} defines the size of window (how many items) we are
34      * trying to keep in the middle. <p>
35      * The middle point is calculated by "scrollCenterOffset" or "scrollCenterOffsetPercent";
36      * if none of these two are defined,  default value is 1/2 of the size.
37      *
38      * @see Axis#setScrollCenterStrategy(int)
39      * @see Axis#getSystemScrollPos(int)
40      */
41     public final static int SCROLL_CENTER_IN_MIDDLE = 0;
42 
43     /**
44      * focus view kept at a fixed location, might see blank space. The distance of fixed location
45      * to left/top is given by {@link Axis#setScrollCenterOffset(int)}
46      *
47      * @see Axis#setScrollCenterStrategy(int)
48      * @see Axis#getSystemScrollPos(int)
49      */
50     public final static int SCROLL_CENTER_FIXED = 1;
51 
52     /**
53      * focus view kept at a fixed percentage distance from the left/top of the view,
54      * might see blank space. The offset percent is set by
55      * {@link Axis#setScrollCenterOffsetPercent(int)}. A fixed offset from this
56      * position may also be set with {@link Axis#setScrollCenterOffset(int)}.
57      *
58      * @see Axis#setScrollCenterStrategy(int)
59      * @see Axis#getSystemScrollPos(int)
60      */
61     public final static int SCROLL_CENTER_FIXED_PERCENT = 2;
62 
63     /**
64      * focus view kept at a fixed location, might see blank space. The distance of fixed location
65      * to right/bottom is given by {@link Axis#setScrollCenterOffset(int)}
66      *
67      * @see Axis#setScrollCenterStrategy(int)
68      * @see Axis#getSystemScrollPos(int)
69      */
70     public final static int SCROLL_CENTER_FIXED_TO_END = 3;
71 
72     /**
73      * Align center of the item
74      */
75     public final static int SCROLL_ITEM_ALIGN_CENTER = 0;
76 
77     /**
78      * Align left/top of the item
79      */
80     public final static int SCROLL_ITEM_ALIGN_LOW = 1;
81 
82     /**
83      * Align right/bottom of the item
84      */
85     public final static int SCROLL_ITEM_ALIGN_HIGH = 2;
86 
87     /** operation not allowed */
88     public final static int OPERATION_DISABLE = 0;
89 
90     /**
91      * operation is using {@link Axis#mScrollMin} {@link Axis#mScrollMax}, see description in
92      * {@link Axis#mScrollCenter}
93      */
94     public final static int OPERATION_NOTOUCH = 1;
95 
96     /**
97      * operation is using {@link Axis#mTouchScrollMax} and {@link Axis#mTouchScrollMin}, see
98      * description in {@link Axis#mScrollCenter}
99      */
100     public final static int OPERATION_TOUCH = 2;
101 
102     /**
103      * maps to OPERATION_TOUCH for touchscreen, OPERATION_NORMAL for non-touchscreen
104      */
105     public final static int OPERATION_AUTO = 3;
106 
107     private static final int SCROLL_DURATION_MIN = 250;
108     private static final int SCROLL_DURATION_MAX = 1500;
109     private static final int SCROLL_DURATION_PAGE_MIN = 250;
110     // millisecond per pixel
111     private static final float SCROLL_DURATION_MS_PER_PIX = 0.25f;
112 
113     /**
114      * Maintains scroll information in one direction
115      */
116     public static class Axis {
117         private int mOperationMode = OPERATION_NOTOUCH;
118         /**
119          * In {@link ScrollController#OPERATION_TOUCH} mode:<br>
120          * {@link #mScrollCenter} changes from {@link #mTouchScrollMin} and
121          * {@link #mTouchScrollMax}; focus won't moved to two sides when scroll to edge of view
122          * port.
123          * <p>
124          * In {@link ScrollController#OPERATION_NOTOUCH} mode:<br>
125          * mScrollCenter changes from {@link #mScrollMin} and {@link #mScrollMax}. It is different
126          * than {@link View#getScrollX()} which starts from left edge of first child; mScrollCenter
127          * starts from center of first child, ends at center of last child; expanded views are
128          * excluded from calculating the mScrollCenter. We convert the mScrollCenter to system
129          * scroll position (see {@link ScrollAdapterView#adjustSystemScrollPos}), note it's not
130          * necessarily a linear transformation between system scrollX and mScrollCenter. <br>
131          * For {@link #SCROLL_CENTER_IN_MIDDLE}: <br>
132          * When mScrollCenter is close to {@link #mScrollMin}, {@link View#getScrollX()} will be
133          * fixed 0, but mScrollCenter is still decreasing, so we can move focus from the item which
134          * is at center of screen to the first child. <br>
135          * For {@link #SCROLL_CENTER_FIXED} and
136          * {@link #SCROLL_CENTER_FIXED_PERCENT}: It's a easy linear conversion
137          * applied
138          * <p>
139          * mScrollCenter is also used to calculate dynamic transformation based on how far a view
140          * is from the mScrollCenter. For example, the views with center close to mScrollCenter
141          * will be scaled up in {@link ScrollAdapterView#applyTransformations}
142          */
143         private float mScrollCenter;
144         /**
145          * Maximum scroll value, initially unlimited until we will get the value when scroll to the
146          * last item of ListAdapter and set the value to center of last child
147          */
148         private int mScrollMax;
149         /**
150          * scroll max for standard touch friendly operation, i.e. focus will not move to side when
151          * scroll to two edges.
152          */
153         private int mTouchScrollMax;
154         /** right/bottom edge of last child */
155         private int mMaxEdge;
156         /** left/top edge of first child, typically should be zero*/
157         private int mMinEdge;
158         /** Minimum scroll value, point to center of first child, typically half of child size */
159         private int mScrollMin;
160         /**
161          * scroll min for standard touch friendly operation, i.e. focus will not move to side when
162          * scroll to two edges.
163          */
164         private int mTouchScrollMin;
165 
166         private int mScrollItemAlign = SCROLL_ITEM_ALIGN_CENTER;
167 
168         private boolean mSelectedTakesMoreSpace = false;
169 
170         /** the offset set by a mouse dragging event */
171         private float mDragOffset;
172 
173         /**
174          * Total extra spaces.  Divided into four parts:<p>
175          * 1.  extraSpace before scrollPosition, given by {@link #mExtraSpaceLow}
176          *     This value is animating from the extra space of "transition from" to the value
177          *     of "transition to"<p>
178          * 2.  extraSpace after scrollPosition<p>
179          * 3.  size of expanded view of "transition from"<p>
180          * 4.  size of expanded view of "transition to"<p>
181          * Among the four parts: 2,3,4 are after scroll position.<p>
182          * 3,4 are included in mExpandedSize when {@link #mSelectedTakesMoreSpace} is true<p>
183          * */
184         private int mExpandedSize;
185         /** extra space used before the scroll position */
186         private int mExtraSpaceLow;
187         private int mExtraSpaceHigh;
188 
189         private int mAlignExtraOffset;
190 
191         /**
192          * Describes how to put the mScrollCenter in the view port different types affects how to
193          * translate mScrollCenter to system scroll position, see details in getSystemScrollPos().
194          */
195         private int mScrollCenterStrategy;
196 
197         /**
198          * used when {@link #mScrollCenterStrategy} is
199          * {@link #SCROLL_CENTER_FIXED}, {@link #SCROLL_CENTER_FIXED_PERCENT} or
200          * {@link #SCROLL_CENTER_FIXED_TO_END}, the offset for the fixed location of center
201          * scroll position relative to left/top,  percentage or right/bottom
202          */
203         private int mScrollCenterOffset = -1;
204 
205         /**
206          * used when {@link #mScrollCenterStrategy} is
207          * {@link #SCROLL_CENTER_FIXED_PERCENT}. The ratio of the view's height
208          * at which to place the scroll center from the top.
209          */
210         private float mScrollCenterOffsetPercent = -1;
211 
212         /** represents position information of child views, see {@link ItemWindow} */
213         public static class Item {
214 
215             private int mIndex;
216             private int mLow;
217             private int mHigh;
218             private int mCenter;
219 
Item()220             public Item() {
221                 mIndex = -1;
222             }
223 
getLow()224             final public int getLow() {
225                 return mLow;
226             }
227 
getHigh()228             final public int getHigh() {
229                 return mHigh;
230             }
231 
getCenter()232             final public int getCenter() {
233                 return mCenter;
234             }
235 
getIndex()236             final public int getIndex() {
237                 return mIndex;
238             }
239 
240             /** set low bound, high bound and index for the item */
setValue(int index, int low, int high)241             final public void setValue(int index, int low, int high) {
242                 mIndex = index;
243                 mLow = low;
244                 mHigh = high;
245                 mCenter = (low + high) / 2;
246             }
247 
isValid()248             final public boolean isValid() {
249                 return mIndex >= 0;
250             }
251 
252             @Override
toString()253             final public String toString() {
254                 return mIndex + "[" + mLow + "," + mHigh + "]";
255             }
256         }
257 
258         private int mSize;
259 
260         private int mPaddingLow;
261 
262         private int mPaddingHigh;
263 
264         private Lerper mLerper;
265 
266         private String mName; // for debugging
267 
Axis(Lerper lerper, String name)268         public Axis(Lerper lerper, String name) {
269             mScrollCenterStrategy = SCROLL_CENTER_IN_MIDDLE;
270             mLerper = lerper;
271             reset();
272             mName = name;
273         }
274 
getScrollCenterStrategy()275         final public int getScrollCenterStrategy() {
276             return mScrollCenterStrategy;
277         }
278 
setScrollCenterStrategy(int scrollCenterStrategy)279         final public void setScrollCenterStrategy(int scrollCenterStrategy) {
280             mScrollCenterStrategy = scrollCenterStrategy;
281         }
282 
getScrollCenterOffset()283         final public int getScrollCenterOffset() {
284             return mScrollCenterOffset;
285         }
286 
setScrollCenterOffset(int scrollCenterOffset)287         final public void setScrollCenterOffset(int scrollCenterOffset) {
288             mScrollCenterOffset = scrollCenterOffset;
289         }
290 
setScrollCenterOffsetPercent(int scrollCenterOffsetPercent)291         final public void setScrollCenterOffsetPercent(int scrollCenterOffsetPercent) {
292             if (scrollCenterOffsetPercent < 0) {
293                 scrollCenterOffsetPercent = 0;
294             } else if (scrollCenterOffsetPercent > 100) {
295                 scrollCenterOffsetPercent = 100;
296             }
297             mScrollCenterOffsetPercent =  ( scrollCenterOffsetPercent / 100.0f);
298         }
299 
setSelectedTakesMoreSpace(boolean selectedTakesMoreSpace)300         final public void setSelectedTakesMoreSpace(boolean selectedTakesMoreSpace) {
301             mSelectedTakesMoreSpace = selectedTakesMoreSpace;
302         }
303 
getSelectedTakesMoreSpace()304         final public boolean getSelectedTakesMoreSpace() {
305             return mSelectedTakesMoreSpace;
306         }
307 
setScrollItemAlign(int align)308         final public void setScrollItemAlign(int align) {
309             mScrollItemAlign = align;
310         }
311 
getScrollItemAlign()312         final public int getScrollItemAlign() {
313             return mScrollItemAlign;
314         }
315 
getScrollCenter()316         final public int getScrollCenter() {
317             return (int) mScrollCenter;
318         }
319 
setOperationMode(int mode)320         final public void setOperationMode(int mode) {
321             mOperationMode = mode;
322         }
323 
scrollMin()324         private int scrollMin() {
325             return mOperationMode == OPERATION_TOUCH ? mTouchScrollMin : mScrollMin;
326         }
327 
scrollMax()328         private int scrollMax() {
329             return mOperationMode == OPERATION_TOUCH ? mTouchScrollMax : mScrollMax;
330         }
331 
332         /** update scroll min and minEdge,  Integer.MIN_VALUE means unknown*/
updateScrollMin(int scrollMin, int minEdge)333         final public void updateScrollMin(int scrollMin, int minEdge) {
334             mScrollMin = scrollMin;
335             if (mScrollCenter < mScrollMin) {
336                 mScrollCenter = mScrollMin;
337             }
338             mMinEdge = minEdge;
339             if (mScrollCenterStrategy != SCROLL_CENTER_IN_MIDDLE
340                     || mScrollMin == Integer.MIN_VALUE) {
341                 mTouchScrollMin = mScrollMin;
342             } else {
343                 mTouchScrollMin = Math.max(mScrollMin, mMinEdge + mSize / 2);
344             }
345         }
346 
invalidateScrollMin()347         public void invalidateScrollMin() {
348             mScrollMin = Integer.MIN_VALUE;
349             mMinEdge = Integer.MIN_VALUE;
350             mTouchScrollMin = Integer.MIN_VALUE;
351         }
352 
353         /** update scroll max and maxEdge,  Integer.MAX_VALUE means unknown*/
updateScrollMax(int scrollMax, int maxEdge)354         final public void updateScrollMax(int scrollMax, int maxEdge) {
355             mScrollMax = scrollMax;
356             if (mScrollCenter > mScrollMax) {
357                 mScrollCenter = mScrollMax;
358             }
359             mMaxEdge = maxEdge;
360             if (mScrollCenterStrategy != SCROLL_CENTER_IN_MIDDLE
361                     || mScrollMax == Integer.MAX_VALUE) {
362                 mTouchScrollMax = mScrollMax;
363             } else {
364                 mTouchScrollMax = Math.min(mScrollMax, mMaxEdge - mSize / 2);
365             }
366         }
367 
invalidateScrollMax()368         public void invalidateScrollMax() {
369             mScrollMax = Integer.MAX_VALUE;
370             mMaxEdge = Integer.MAX_VALUE;
371             mTouchScrollMax = Integer.MAX_VALUE;
372         }
373 
canScroll(boolean forward)374         final public boolean canScroll(boolean forward) {
375             if (forward) {
376                 if (mScrollCenter >= mScrollMax) {
377                     return false;
378                 }
379             } else {
380                 if (mScrollCenter <= mScrollMin) {
381                     return false;
382                 }
383             }
384             return true;
385         }
386 
updateScrollCenter(float scrollTarget, boolean lerper)387         private boolean updateScrollCenter(float scrollTarget, boolean lerper) {
388             mDragOffset = 0;
389             int scrollMin = scrollMin();
390             int scrollMax = scrollMax();
391             boolean overScroll = false;
392             if (scrollMin >= scrollMax) {
393                 scrollTarget = mScrollCenter;
394                 overScroll = true;
395             } else if (scrollTarget < scrollMin) {
396                 scrollTarget = scrollMin;
397                 overScroll = true;
398             } else if (scrollTarget > scrollMax) {
399                 scrollTarget = scrollMax;
400                 overScroll = true;
401             }
402             if (lerper) {
403                 mScrollCenter = mLerper.getValue(mScrollCenter, scrollTarget);
404             } else {
405                 mScrollCenter = scrollTarget;
406             }
407             return overScroll;
408         }
409 
updateFromDrag()410         private void updateFromDrag() {
411             updateScrollCenter(mScrollCenter + mDragOffset, false);
412         }
413 
dragBy(float distanceX)414         private void dragBy(float distanceX) {
415             mDragOffset += distanceX;
416         }
417 
reset()418         private void reset() {
419             mScrollCenter = Integer.MIN_VALUE;
420             mScrollMin = Integer.MIN_VALUE;
421             mMinEdge = Integer.MIN_VALUE;
422             mTouchScrollMin = Integer.MIN_VALUE;
423             mScrollMax = Integer.MAX_VALUE;
424             mMaxEdge = Integer.MAX_VALUE;
425             mTouchScrollMax = Integer.MAX_VALUE;
426             mExpandedSize = 0;
427             mDragOffset = 0;
428         }
429 
isMinUnknown()430         final public boolean isMinUnknown() {
431             return mScrollMin == Integer.MIN_VALUE;
432         }
433 
isMaxUnknown()434         final public boolean isMaxUnknown() {
435             return mScrollMax == Integer.MAX_VALUE;
436         }
437 
getSizeForExpandableItem()438         final public int getSizeForExpandableItem() {
439             return mSize - mPaddingLow - mPaddingHigh - mExpandedSize;
440         }
441 
setSize(int size)442         final public void setSize(int size) {
443             mSize = size;
444         }
445 
setExpandedSize(int expandedSize)446         final public void setExpandedSize(int expandedSize) {
447             mExpandedSize = expandedSize;
448         }
449 
setExtraSpaceLow(int extraSpaceLow)450         final public void setExtraSpaceLow(int extraSpaceLow) {
451             mExtraSpaceLow = extraSpaceLow;
452         }
453 
setExtraSpaceHigh(int extraSpaceHigh)454         final public void setExtraSpaceHigh(int extraSpaceHigh) {
455             mExtraSpaceHigh = extraSpaceHigh;
456         }
457 
setAlignExtraOffset(int extraOffset)458         final public void setAlignExtraOffset(int extraOffset) {
459             mAlignExtraOffset = extraOffset;
460         }
461 
setPadding(int paddingLow, int paddingHigh)462         final public void setPadding(int paddingLow, int paddingHigh) {
463             mPaddingLow = paddingLow;
464             mPaddingHigh = paddingHigh;
465         }
466 
getPaddingLow()467         final public int getPaddingLow() {
468             return mPaddingLow;
469         }
470 
getPaddingHigh()471         final public int getPaddingHigh() {
472             return mPaddingHigh;
473         }
474 
getSystemScrollPos()475         final public int getSystemScrollPos() {
476             return getSystemScrollPos((int) mScrollCenter);
477         }
478 
getSystemScrollPos(int scrollCenter)479         final public int getSystemScrollPos(int scrollCenter) {
480             scrollCenter += mAlignExtraOffset;
481 
482             // For the "FIXED" strategy family:
483             int compensate = mSelectedTakesMoreSpace ? mExtraSpaceLow : -mExtraSpaceLow;
484             if (mScrollCenterStrategy == SCROLL_CENTER_FIXED) {
485                 return scrollCenter - mScrollCenterOffset + compensate;
486             } else if (mScrollCenterStrategy == SCROLL_CENTER_FIXED_TO_END) {
487                 return scrollCenter - (mSize - mScrollCenterOffset) + compensate;
488             } else if (mScrollCenterStrategy == SCROLL_CENTER_FIXED_PERCENT) {
489                 return (int) (scrollCenter - mScrollCenterOffset - mSize
490                         * mScrollCenterOffsetPercent) + compensate;
491             }
492             int clientSize = mSize - mPaddingLow - mPaddingHigh;
493             // For SCROLL_CENTER_IN_MIDDLE, first calculate the middle point:
494             // if the scrollCenterOffset or scrollCenterOffsetPercent is specified,
495             // use it for middle point,  otherwise, use 1/2 of the size
496             int middlePosition;
497             if (mScrollCenterOffset >= 0) {
498                 middlePosition = mScrollCenterOffset - mPaddingLow;
499             } else if (mScrollCenterOffsetPercent >= 0) {
500                 middlePosition = (int) (mSize * mScrollCenterOffsetPercent) - mPaddingLow;
501             } else {
502                 middlePosition = clientSize / 2;
503             }
504             int afterMiddlePosition = clientSize - middlePosition;
505             // Following code for mSelectedTakesMoreSpace = true/false is quite similar,
506             // but it's still more clear and easier to debug when separating them.
507             boolean isMinUnknown = isMinUnknown();
508             boolean isMaxUnknown = isMaxUnknown();
509             if (mSelectedTakesMoreSpace) {
510                 int extraSpaceLow;
511                 switch (getScrollItemAlign()) {
512                     case SCROLL_ITEM_ALIGN_LOW:
513                         extraSpaceLow = 0;
514                         break;
515                     case SCROLL_ITEM_ALIGN_HIGH:
516                         extraSpaceLow = mExtraSpaceLow + mExtraSpaceHigh;
517                         break;
518                     case SCROLL_ITEM_ALIGN_CENTER:
519                     default:
520                         extraSpaceLow = mExtraSpaceLow;
521                         break;
522                 }
523                 if (!isMinUnknown && !isMaxUnknown) {
524                     if (mMaxEdge - mMinEdge + mExpandedSize <= clientSize) {
525                         // total children size is less than view port: align the left edge
526                         // of first child to view port's left edge
527                         return mMinEdge - mPaddingLow;
528                     }
529                 }
530                 if (!isMinUnknown) {
531                     if (scrollCenter - mMinEdge + extraSpaceLow <= middlePosition) {
532                         // scroll center is within half of view port size: align the left edge
533                         // of first child to the left edge of view port
534                         return mMinEdge - mPaddingLow;
535                     }
536                 }
537                 if (!isMaxUnknown) {
538                     int spaceAfterScrollCenter = mExpandedSize - extraSpaceLow;
539                     if (mMaxEdge - scrollCenter + spaceAfterScrollCenter <= afterMiddlePosition) {
540                         // scroll center is very close to the right edge of view port : align the
541                         // right edge of last children (plus expanded size) to view port's right
542                         return mMaxEdge -mPaddingLow - (clientSize - mExpandedSize );
543                     }
544                 }
545                 // else put scroll center in middle of view port
546                 return scrollCenter - middlePosition - mPaddingLow + extraSpaceLow;
547             } else {
548                 int shift;
549                 switch (getScrollItemAlign()) {
550                     case SCROLL_ITEM_ALIGN_LOW:
551                         shift = - mExtraSpaceLow;
552                         break;
553                     case SCROLL_ITEM_ALIGN_HIGH:
554                         shift = + mExtraSpaceHigh;
555                         break;
556                     case SCROLL_ITEM_ALIGN_CENTER:
557                     default:
558                         shift = 0;
559                         break;
560                 }
561                 if (!isMinUnknown && !isMaxUnknown) {
562                     if (mMaxEdge - mMinEdge + mExpandedSize <= clientSize) {
563                         // total children size is less than view port: align the left edge
564                         // of first child to view port's left edge
565                         return mMinEdge - mPaddingLow;
566                     }
567                 }
568                 if (!isMinUnknown) {
569                     if (scrollCenter + shift - mMinEdge <= middlePosition) {
570                         // scroll center is within half of view port size: align the left edge
571                         // of first child to the left edge of view port
572                         return mMinEdge - mPaddingLow;
573                     }
574                 }
575                 if (!isMaxUnknown) {
576                     if (mMaxEdge - scrollCenter - shift + mExpandedSize <= afterMiddlePosition) {
577                         // scroll center is very close to the right edge of view port : align the
578                         // right edge of last children (plus expanded size) to view port's right
579                         return mMaxEdge -mPaddingLow - (clientSize - mExpandedSize );
580                     }
581                 }
582                 // else put scroll center in middle of view port
583                 return scrollCenter - middlePosition - mPaddingLow + shift;
584             }
585         }
586 
587         @Override
toString()588         public String toString() {
589             return "center: " + mScrollCenter + " min:" + mMinEdge + "," + mScrollMin +
590                     " max:" + mScrollMax + "," + mMaxEdge;
591         }
592 
593     }
594 
595     private Context mContext;
596 
597     // we separate Scrollers for scroll animation and fling animation; this is because we want a
598     // flywheel feature for fling animation, ScrollAdapterView inserts scroll animation between
599     // fling animations, the fling animation will mistakenly continue the old velocity of scroll
600     // animation: that's wrong, we want fling animation pickup the old velocity of last fling.
601     private Scroller mScrollScroller;
602     private Scroller mFlingScroller;
603 
604     private final static int STATE_NONE = 0;
605 
606     /** using fling scroller */
607     private final static int STATE_FLING = 1;
608 
609     /** using scroll scroller */
610     private final static int STATE_SCROLL = 2;
611 
612     /** using drag */
613     private final static int STATE_DRAG = 3;
614 
615     private int mState = STATE_NONE;
616 
617     private int mOrientation = ScrollAdapterView.HORIZONTAL;
618 
619     private Lerper mLerper = new Lerper();
620 
621     final public Axis vertical = new Axis(mLerper, "vertical");
622 
623     final public Axis horizontal = new Axis(mLerper, "horizontal");
624 
625     private Axis mMainAxis = horizontal;
626 
627     private Axis mSecondAxis = vertical;
628 
629     /** fling operation mode */
630     private int mFlingMode = OPERATION_AUTO;
631 
632     /** drag operation mode */
633     private int mDragMode = OPERATION_AUTO;
634 
635     /** scroll operation mode (for DPAD) */
636     private int mScrollMode = OPERATION_NOTOUCH;
637 
638     /** the major movement is in horizontal or vertical */
639     private boolean mMainHorizontal;
640     private boolean mHorizontalForward = true;
641     private boolean mVerticalForward = true;
642 
lerper()643     final public Lerper lerper() {
644         return mLerper;
645     }
646 
mainAxis()647     final public Axis mainAxis() {
648         return mMainAxis;
649     }
650 
secondAxis()651     final public Axis secondAxis() {
652         return mSecondAxis;
653     }
654 
setLerperDivisor(float divisor)655     final public void setLerperDivisor(float divisor) {
656         mLerper.setDivisor(divisor);
657     }
658 
ScrollController(Context context)659     public ScrollController(Context context) {
660         mContext = context;
661         // Quint easeOut
662         mScrollScroller = new Scroller(mContext, new DecelerateInterpolator(2));
663         mFlingScroller = new Scroller(mContext, new LinearInterpolator());
664     }
665 
setOrientation(int orientation)666     final public void setOrientation(int orientation) {
667         int align = mainAxis().getScrollItemAlign();
668         boolean selectedTakesMoreSpace = mainAxis().getSelectedTakesMoreSpace();
669         mOrientation = orientation;
670         if (mOrientation == ScrollAdapterView.HORIZONTAL) {
671             mMainAxis = horizontal;
672             mSecondAxis = vertical;
673         } else {
674             mMainAxis = vertical;
675             mSecondAxis = horizontal;
676         }
677         mMainAxis.setScrollItemAlign(align);
678         mSecondAxis.setScrollItemAlign(SCROLL_ITEM_ALIGN_CENTER);
679         mMainAxis.setSelectedTakesMoreSpace(selectedTakesMoreSpace);
680         mSecondAxis.setSelectedTakesMoreSpace(false);
681     }
682 
setScrollItemAlign(int align)683     public void setScrollItemAlign(int align) {
684         mainAxis().setScrollItemAlign(align);
685     }
686 
getScrollItemAlign()687     public int getScrollItemAlign() {
688         return mainAxis().getScrollItemAlign();
689     }
690 
getOrientation()691     final public int getOrientation() {
692         return mOrientation;
693     }
694 
getFlingMode()695     final public int getFlingMode() {
696         return mFlingMode;
697     }
698 
setFlingMode(int mode)699     final public void setFlingMode(int mode) {
700         this.mFlingMode = mode;
701     }
702 
getDragMode()703     final public int getDragMode() {
704         return mDragMode;
705     }
706 
setDragMode(int mode)707     final public void setDragMode(int mode) {
708         this.mDragMode = mode;
709     }
710 
getScrollMode()711     final public int getScrollMode() {
712         return mScrollMode;
713     }
714 
setScrollMode(int mode)715     final public void setScrollMode(int mode) {
716         this.mScrollMode = mode;
717     }
718 
getCurrVelocity()719     final public float getCurrVelocity() {
720         if (mState == STATE_FLING) {
721             return mFlingScroller.getCurrVelocity();
722         } else if (mState == STATE_SCROLL) {
723             return mScrollScroller.getCurrVelocity();
724         }
725         return 0;
726     }
727 
canScroll(int dx, int dy)728     final public boolean canScroll(int dx, int dy) {
729         if (dx == 0 && dy == 0) {
730             return false;
731         }
732         return (dx == 0 || horizontal.canScroll(dx < 0)) &&
733                 (dy == 0 || vertical.canScroll(dy < 0));
734     }
735 
getMode(int mode)736     private int getMode(int mode) {
737         if (mode == OPERATION_AUTO) {
738             if (mContext.getResources().getConfiguration().touchscreen
739                     == Configuration.TOUCHSCREEN_NOTOUCH) {
740                 mode = OPERATION_NOTOUCH;
741             } else {
742                 mode = OPERATION_TOUCH;
743             }
744         }
745         return mode;
746     }
747 
updateDirection(float dx, float dy)748     private void updateDirection(float dx, float dy) {
749         mMainHorizontal = Math.abs(dx) >= Math.abs(dy);
750         if (dx > 0) {
751             mHorizontalForward = true;
752         } else if (dx < 0) {
753             mHorizontalForward = false;
754         }
755         if (dy > 0) {
756             mVerticalForward = true;
757         } else if (dy < 0) {
758             mVerticalForward = false;
759         }
760     }
761 
fling(int velocity_x, int velocity_y)762     final public boolean fling(int velocity_x, int velocity_y){
763         if (mFlingMode == OPERATION_DISABLE) {
764             return false;
765         }
766         final int operationMode = getMode(mFlingMode);
767         horizontal.setOperationMode(operationMode);
768         vertical.setOperationMode(operationMode);
769         mState = STATE_FLING;
770         mFlingScroller.fling((int)(horizontal.mScrollCenter),
771                 (int)(vertical.mScrollCenter),
772                 velocity_x,
773                 velocity_y,
774                 Integer.MIN_VALUE,
775                 Integer.MAX_VALUE,
776                 Integer.MIN_VALUE,
777                 Integer.MAX_VALUE);
778         updateDirection(velocity_x, velocity_y);
779         return true;
780     }
781 
startScroll(int dx, int dy, boolean easeFling, int duration, boolean page)782     final public void startScroll(int dx, int dy, boolean easeFling, int duration, boolean page) {
783         if (mScrollMode == OPERATION_DISABLE) {
784             return;
785         }
786         final int operationMode = getMode(mScrollMode);
787         horizontal.setOperationMode(operationMode);
788         vertical.setOperationMode(operationMode);
789         Scroller scroller;
790         if (easeFling) {
791             mState = STATE_FLING;
792             scroller = mFlingScroller;
793         } else {
794             mState = STATE_SCROLL;
795             scroller = mScrollScroller;
796         }
797         int basex = horizontal.getScrollCenter();
798         int basey = vertical.getScrollCenter();
799         if (!scroller.isFinished()) {
800             // during scrolling, we should continue from getCurrX/getCurrY() (might be different
801             // than current Scroll Center due to Lerper)
802             dx = basex + dx - scroller.getCurrX();
803             dy = basey + dy - scroller.getCurrY();
804             basex = scroller.getCurrX();
805             basey = scroller.getCurrY();
806         }
807         updateDirection(dx, dy);
808         if (easeFling) {
809             float curDx = Math.abs(mFlingScroller.getFinalX() - mFlingScroller.getStartX());
810             float curDy = Math.abs(mFlingScroller.getFinalY() - mFlingScroller.getStartY());
811             float hyp = (float) Math.sqrt(curDx * curDx + curDy * curDy);
812             float velocity = mFlingScroller.getCurrVelocity();
813             float velocityX = velocity * curDx / hyp;
814             float velocityY = velocity * curDy / hyp;
815             int durationX = velocityX ==0 ? 0 : (int)((Math.abs(dx) * 1000) / velocityX);
816             int durationY = velocityY ==0 ? 0 : (int)((Math.abs(dy) * 1000) / velocityY);
817             if (duration == 0) duration = Math.max(durationX, durationY);
818         } else {
819             if (duration == 0) {
820                 duration = getScrollDuration((int) Math.sqrt(dx * dx + dy * dy), page);
821             }
822         }
823         scroller.startScroll(basex, basey, dx, dy, duration);
824     }
825 
getCurrentAnimationDuration()826     final public int getCurrentAnimationDuration() {
827         Scroller scroller;
828         if (mState == STATE_FLING) {
829             scroller = mFlingScroller;
830         } else if (mState == STATE_SCROLL) {
831             scroller = mScrollScroller;
832         } else {
833             return 0;
834         }
835         return scroller.getDuration();
836     }
837 
startScrollByMain(int deltaMain, int deltaSecond, boolean easeFling, int duration, boolean page)838     final public void startScrollByMain(int deltaMain, int deltaSecond, boolean easeFling,
839             int duration, boolean page) {
840         int dx, dy;
841         if (mOrientation == ScrollAdapterView.HORIZONTAL) {
842             dx = deltaMain;
843             dy = deltaSecond;
844         } else {
845             dx = deltaSecond;
846             dy = deltaMain;
847         }
848         startScroll(dx, dy, easeFling, duration, page);
849     }
850 
dragBy(float distanceX, float distanceY)851     final public boolean dragBy(float distanceX, float distanceY) {
852         if (mDragMode == OPERATION_DISABLE) {
853             return false;
854         }
855         final int operationMode = getMode(mDragMode);
856         horizontal.setOperationMode(operationMode);
857         vertical.setOperationMode(operationMode);
858         horizontal.dragBy(distanceX);
859         vertical.dragBy(distanceY);
860         mState = STATE_DRAG;
861         return true;
862     }
863 
stopDrag()864     final public void stopDrag() {
865         mState = STATE_NONE;
866         vertical.mDragOffset = 0;
867         horizontal.mDragOffset = 0;
868     }
869 
setScrollCenterByMain(int centerMain, int centerSecond)870     final public void setScrollCenterByMain(int centerMain, int centerSecond) {
871         if (mOrientation == ScrollAdapterView.HORIZONTAL) {
872             setScrollCenter(centerMain, centerSecond);
873         } else {
874             setScrollCenter(centerSecond, centerMain);
875         }
876     }
877 
setScrollCenter(int centerX, int centerY)878     final public void setScrollCenter(int centerX, int centerY) {
879         horizontal.updateScrollCenter(centerX, false);
880         vertical.updateScrollCenter(centerY, false);
881         // centerX, centerY might be clipped by min/max
882         centerX = horizontal.getScrollCenter();
883         centerY = vertical.getScrollCenter();
884         mFlingScroller.setFinalX(centerX);
885         mFlingScroller.setFinalY(centerY);
886         mFlingScroller.abortAnimation();
887         mScrollScroller.setFinalX(centerX);
888         mScrollScroller.setFinalY(centerY);
889         mScrollScroller.abortAnimation();
890     }
891 
getFinalX()892     final public int getFinalX() {
893         if (mState == STATE_FLING) {
894             return mFlingScroller.getFinalX();
895         } else if (mState == STATE_SCROLL) {
896             return mScrollScroller.getFinalX();
897         }
898         return horizontal.getScrollCenter();
899     }
900 
getFinalY()901     final public int getFinalY() {
902         if (mState == STATE_FLING) {
903             return mFlingScroller.getFinalY();
904         } else if (mState == STATE_SCROLL) {
905             return mScrollScroller.getFinalY();
906         }
907         return vertical.getScrollCenter();
908     }
909 
setFinalX(int finalX)910     final public void setFinalX(int finalX) {
911         if (mState == STATE_FLING) {
912             mFlingScroller.setFinalX(finalX);
913         } else if (mState == STATE_SCROLL) {
914             mScrollScroller.setFinalX(finalX);
915         }
916     }
917 
setFinalY(int finalY)918     final public void setFinalY(int finalY) {
919         if (mState == STATE_FLING) {
920             mFlingScroller.setFinalY(finalY);
921         } else if (mState == STATE_SCROLL) {
922             mScrollScroller.setFinalY(finalY);
923         }
924     }
925 
926     /** return true if scroll/fling animation or lerper is not stopped */
isFinished()927     final public boolean isFinished() {
928         Scroller scroller;
929         if (mState == STATE_FLING) {
930             scroller = mFlingScroller;
931         } else if (mState == STATE_SCROLL) {
932             scroller = mScrollScroller;
933         } else if (mState == STATE_DRAG){
934             return false;
935         } else {
936             return true;
937         }
938         if (scroller.isFinished()) {
939             return (horizontal.getScrollCenter() == scroller.getCurrX() &&
940                     vertical.getScrollCenter() == scroller.getCurrY());
941         }
942         return false;
943     }
944 
isMainAxisMovingForward()945     final public boolean isMainAxisMovingForward() {
946         return mOrientation == ScrollAdapterView.HORIZONTAL ?
947                 mHorizontalForward : mVerticalForward;
948     }
949 
isSecondAxisMovingForward()950     final public boolean isSecondAxisMovingForward() {
951         return mOrientation == ScrollAdapterView.HORIZONTAL ?
952                 mVerticalForward : mHorizontalForward;
953     }
954 
getLastDirection()955     final public int getLastDirection() {
956         if (mMainHorizontal) {
957             return mHorizontalForward ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
958         } else {
959             return mVerticalForward ? View.FOCUS_DOWN : View.FOCUS_UP;
960         }
961     }
962 
963     /**
964      * update scroller position, this is either trigger by fling()/startScroll() on the
965      * scroller object,  or lerper, or can be caused by a dragBy()
966      */
computeAndSetScrollPosition()967     final public void computeAndSetScrollPosition() {
968         Scroller scroller;
969         if (mState == STATE_FLING) {
970             scroller = mFlingScroller;
971         } else if (mState == STATE_SCROLL) {
972             scroller = mScrollScroller;
973         } else if (mState == STATE_DRAG) {
974             if (horizontal.mDragOffset != 0 || vertical.mDragOffset !=0 ) {
975                 horizontal.updateFromDrag();
976                 vertical.updateFromDrag();
977             }
978             return;
979         } else {
980             return;
981         }
982         if (!isFinished()) {
983             scroller.computeScrollOffset();
984             horizontal.updateScrollCenter(scroller.getCurrX(), true);
985             vertical.updateScrollCenter(scroller.getCurrY(), true);
986         }
987     }
988 
989     /** get Scroll animation duration in ms for given pixels */
getScrollDuration(int distance, boolean isPage)990     final public int getScrollDuration(int distance, boolean isPage) {
991         int ms = (int)(distance * SCROLL_DURATION_MS_PER_PIX);
992         int minValue = isPage ? SCROLL_DURATION_PAGE_MIN : SCROLL_DURATION_MIN;
993         if (ms < minValue) {
994             ms = minValue;
995         } else if (ms > SCROLL_DURATION_MAX) {
996             ms = SCROLL_DURATION_MAX;
997         }
998         return ms;
999     }
1000 
reset()1001     final public void reset() {
1002         mainAxis().reset();
1003     }
1004 
1005     @Override
toString()1006     public String toString() {
1007         return new StringBuffer().append("horizontal=")
1008                 .append(horizontal.toString())
1009                 .append("vertical=")
1010                 .append(vertical.toString())
1011                 .toString();
1012     }
1013 
1014 }
1015