Home | History | Annotate | Download | only in widget
      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 
    220             public Item() {
    221                 mIndex = -1;
    222             }
    223 
    224             final public int getLow() {
    225                 return mLow;
    226             }
    227 
    228             final public int getHigh() {
    229                 return mHigh;
    230             }
    231 
    232             final public int getCenter() {
    233                 return mCenter;
    234             }
    235 
    236             final public int getIndex() {
    237                 return mIndex;
    238             }
    239 
    240             /** set low bound, high bound and index for the item */
    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 
    248             final public boolean isValid() {
    249                 return mIndex >= 0;
    250             }
    251 
    252             @Override
    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 final Lerper mLerper;
    265 
    266         private final String mName; // for debugging
    267 
    268         public Axis(Lerper lerper, String name) {
    269             mScrollCenterStrategy = SCROLL_CENTER_IN_MIDDLE;
    270             mLerper = lerper;
    271             reset();
    272             mName = name;
    273         }
    274 
    275         final public int getScrollCenterStrategy() {
    276             return mScrollCenterStrategy;
    277         }
    278 
    279         final public void setScrollCenterStrategy(int scrollCenterStrategy) {
    280             mScrollCenterStrategy = scrollCenterStrategy;
    281         }
    282 
    283         final public int getScrollCenterOffset() {
    284             return mScrollCenterOffset;
    285         }
    286 
    287         final public void setScrollCenterOffset(int scrollCenterOffset) {
    288             mScrollCenterOffset = scrollCenterOffset;
    289         }
    290 
    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 
    300         final public void setSelectedTakesMoreSpace(boolean selectedTakesMoreSpace) {
    301             mSelectedTakesMoreSpace = selectedTakesMoreSpace;
    302         }
    303 
    304         final public boolean getSelectedTakesMoreSpace() {
    305             return mSelectedTakesMoreSpace;
    306         }
    307 
    308         final public void setScrollItemAlign(int align) {
    309             mScrollItemAlign = align;
    310         }
    311 
    312         final public int getScrollItemAlign() {
    313             return mScrollItemAlign;
    314         }
    315 
    316         final public int getScrollCenter() {
    317             return (int) mScrollCenter;
    318         }
    319 
    320         final public void setOperationMode(int mode) {
    321             mOperationMode = mode;
    322         }
    323 
    324         private int scrollMin() {
    325             return mOperationMode == OPERATION_TOUCH ? mTouchScrollMin : mScrollMin;
    326         }
    327 
    328         private int scrollMax() {
    329             return mOperationMode == OPERATION_TOUCH ? mTouchScrollMax : mScrollMax;
    330         }
    331 
    332         /** update scroll min and minEdge,  Integer.MIN_VALUE means unknown*/
    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 
    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*/
    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 
    368         public void invalidateScrollMax() {
    369             mScrollMax = Integer.MAX_VALUE;
    370             mMaxEdge = Integer.MAX_VALUE;
    371             mTouchScrollMax = Integer.MAX_VALUE;
    372         }
    373 
    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 
    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 
    410         private void updateFromDrag() {
    411             updateScrollCenter(mScrollCenter + mDragOffset, false);
    412         }
    413 
    414         private void dragBy(float distanceX) {
    415             mDragOffset += distanceX;
    416         }
    417 
    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 
    430         final public boolean isMinUnknown() {
    431             return mScrollMin == Integer.MIN_VALUE;
    432         }
    433 
    434         final public boolean isMaxUnknown() {
    435             return mScrollMax == Integer.MAX_VALUE;
    436         }
    437 
    438         final public int getSizeForExpandableItem() {
    439             return mSize - mPaddingLow - mPaddingHigh - mExpandedSize;
    440         }
    441 
    442         final public void setSize(int size) {
    443             mSize = size;
    444         }
    445 
    446         final public void setExpandedSize(int expandedSize) {
    447             mExpandedSize = expandedSize;
    448         }
    449 
    450         final public void setExtraSpaceLow(int extraSpaceLow) {
    451             mExtraSpaceLow = extraSpaceLow;
    452         }
    453 
    454         final public void setExtraSpaceHigh(int extraSpaceHigh) {
    455             mExtraSpaceHigh = extraSpaceHigh;
    456         }
    457 
    458         final public void setAlignExtraOffset(int extraOffset) {
    459             mAlignExtraOffset = extraOffset;
    460         }
    461 
    462         final public void setPadding(int paddingLow, int paddingHigh) {
    463             mPaddingLow = paddingLow;
    464             mPaddingHigh = paddingHigh;
    465         }
    466 
    467         final public int getPaddingLow() {
    468             return mPaddingLow;
    469         }
    470 
    471         final public int getPaddingHigh() {
    472             return mPaddingHigh;
    473         }
    474 
    475         final public int getSystemScrollPos() {
    476             return getSystemScrollPos((int) mScrollCenter);
    477         }
    478 
    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
    588         public String toString() {
    589             return "center: " + mScrollCenter + " min:" + mMinEdge + "," + mScrollMin +
    590                     " max:" + mScrollMax + "," + mMaxEdge;
    591         }
    592 
    593     }
    594 
    595     private final 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 final Scroller mScrollScroller;
    602     private final 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 final 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 
    643     final public Lerper lerper() {
    644         return mLerper;
    645     }
    646 
    647     final public Axis mainAxis() {
    648         return mMainAxis;
    649     }
    650 
    651     final public Axis secondAxis() {
    652         return mSecondAxis;
    653     }
    654 
    655     final public void setLerperDivisor(float divisor) {
    656         mLerper.setDivisor(divisor);
    657     }
    658 
    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 
    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 
    683     public void setScrollItemAlign(int align) {
    684         mainAxis().setScrollItemAlign(align);
    685     }
    686 
    687     public int getScrollItemAlign() {
    688         return mainAxis().getScrollItemAlign();
    689     }
    690 
    691     final public int getOrientation() {
    692         return mOrientation;
    693     }
    694 
    695     final public int getFlingMode() {
    696         return mFlingMode;
    697     }
    698 
    699     final public void setFlingMode(int mode) {
    700         this.mFlingMode = mode;
    701     }
    702 
    703     final public int getDragMode() {
    704         return mDragMode;
    705     }
    706 
    707     final public void setDragMode(int mode) {
    708         this.mDragMode = mode;
    709     }
    710 
    711     final public int getScrollMode() {
    712         return mScrollMode;
    713     }
    714 
    715     final public void setScrollMode(int mode) {
    716         this.mScrollMode = mode;
    717     }
    718 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    864     final public void stopDrag() {
    865         mState = STATE_NONE;
    866         vertical.mDragOffset = 0;
    867         horizontal.mDragOffset = 0;
    868     }
    869 
    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 
    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 
    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 
    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 
    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 
    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 */
    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 
    945     final public boolean isMainAxisMovingForward() {
    946         return mOrientation == ScrollAdapterView.HORIZONTAL ?
    947                 mHorizontalForward : mVerticalForward;
    948     }
    949 
    950     final public boolean isSecondAxisMovingForward() {
    951         return mOrientation == ScrollAdapterView.HORIZONTAL ?
    952                 mVerticalForward : mHorizontalForward;
    953     }
    954 
    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      */
    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 */
    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 
   1001     final public void reset() {
   1002         mainAxis().reset();
   1003     }
   1004 
   1005     @Override
   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