Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2008 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 android.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.ColorStateList;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Canvas;
     23 import android.graphics.Paint;
     24 import android.graphics.Rect;
     25 import android.graphics.RectF;
     26 import android.graphics.drawable.Drawable;
     27 import android.graphics.drawable.NinePatchDrawable;
     28 import android.os.Handler;
     29 import android.os.SystemClock;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.view.ViewConfiguration;
     33 import android.widget.AbsListView.OnScrollListener;
     34 
     35 /**
     36  * Helper class for AbsListView to draw and control the Fast Scroll thumb
     37  */
     38 class FastScroller {
     39     private static final String TAG = "FastScroller";
     40 
     41     // Minimum number of pages to justify showing a fast scroll thumb
     42     private static int MIN_PAGES = 4;
     43     // Scroll thumb not showing
     44     private static final int STATE_NONE = 0;
     45     // Not implemented yet - fade-in transition
     46     private static final int STATE_ENTER = 1;
     47     // Scroll thumb visible and moving along with the scrollbar
     48     private static final int STATE_VISIBLE = 2;
     49     // Scroll thumb being dragged by user
     50     private static final int STATE_DRAGGING = 3;
     51     // Scroll thumb fading out due to inactivity timeout
     52     private static final int STATE_EXIT = 4;
     53 
     54     private static final int[] PRESSED_STATES = new int[] {
     55         android.R.attr.state_pressed
     56     };
     57 
     58     private static final int[] DEFAULT_STATES = new int[0];
     59 
     60     private static final int[] ATTRS = new int[] {
     61         android.R.attr.fastScrollTextColor,
     62         android.R.attr.fastScrollThumbDrawable,
     63         android.R.attr.fastScrollTrackDrawable,
     64         android.R.attr.fastScrollPreviewBackgroundLeft,
     65         android.R.attr.fastScrollPreviewBackgroundRight,
     66         android.R.attr.fastScrollOverlayPosition
     67     };
     68 
     69     private static final int TEXT_COLOR = 0;
     70     private static final int THUMB_DRAWABLE = 1;
     71     private static final int TRACK_DRAWABLE = 2;
     72     private static final int PREVIEW_BACKGROUND_LEFT = 3;
     73     private static final int PREVIEW_BACKGROUND_RIGHT = 4;
     74     private static final int OVERLAY_POSITION = 5;
     75 
     76     private static final int OVERLAY_FLOATING = 0;
     77     private static final int OVERLAY_AT_THUMB = 1;
     78 
     79     private Drawable mThumbDrawable;
     80     private Drawable mOverlayDrawable;
     81     private Drawable mTrackDrawable;
     82 
     83     private Drawable mOverlayDrawableLeft;
     84     private Drawable mOverlayDrawableRight;
     85 
     86     int mThumbH;
     87     int mThumbW;
     88     int mThumbY;
     89 
     90     private RectF mOverlayPos;
     91     private int mOverlaySize;
     92 
     93     AbsListView mList;
     94     boolean mScrollCompleted;
     95     private int mVisibleItem;
     96     private Paint mPaint;
     97     private int mListOffset;
     98     private int mItemCount = -1;
     99     private boolean mLongList;
    100 
    101     private Object [] mSections;
    102     private String mSectionText;
    103     private boolean mDrawOverlay;
    104     private ScrollFade mScrollFade;
    105 
    106     private int mState;
    107 
    108     private Handler mHandler = new Handler();
    109 
    110     BaseAdapter mListAdapter;
    111     private SectionIndexer mSectionIndexer;
    112 
    113     private boolean mChangedBounds;
    114 
    115     private int mPosition;
    116 
    117     private boolean mAlwaysShow;
    118 
    119     private int mOverlayPosition;
    120 
    121     private boolean mMatchDragPosition;
    122 
    123     float mInitialTouchY;
    124     boolean mPendingDrag;
    125     private int mScaledTouchSlop;
    126 
    127     private static final int FADE_TIMEOUT = 1500;
    128     private static final int PENDING_DRAG_DELAY = 180;
    129 
    130     private final Rect mTmpRect = new Rect();
    131 
    132     private final Runnable mDeferStartDrag = new Runnable() {
    133         public void run() {
    134             if (mList.mIsAttached) {
    135                 beginDrag();
    136 
    137                 final int viewHeight = mList.getHeight();
    138                 // Jitter
    139                 int newThumbY = (int) mInitialTouchY - mThumbH + 10;
    140                 if (newThumbY < 0) {
    141                     newThumbY = 0;
    142                 } else if (newThumbY + mThumbH > viewHeight) {
    143                     newThumbY = viewHeight - mThumbH;
    144                 }
    145                 mThumbY = newThumbY;
    146                 scrollTo((float) mThumbY / (viewHeight - mThumbH));
    147             }
    148 
    149             mPendingDrag = false;
    150         }
    151     };
    152 
    153     public FastScroller(Context context, AbsListView listView) {
    154         mList = listView;
    155         init(context);
    156     }
    157 
    158     public void setAlwaysShow(boolean alwaysShow) {
    159         mAlwaysShow = alwaysShow;
    160         if (alwaysShow) {
    161             mHandler.removeCallbacks(mScrollFade);
    162             setState(STATE_VISIBLE);
    163         } else if (mState == STATE_VISIBLE) {
    164             mHandler.postDelayed(mScrollFade, FADE_TIMEOUT);
    165         }
    166     }
    167 
    168     public boolean isAlwaysShowEnabled() {
    169         return mAlwaysShow;
    170     }
    171 
    172     private void refreshDrawableState() {
    173         int[] state = mState == STATE_DRAGGING ? PRESSED_STATES : DEFAULT_STATES;
    174 
    175         if (mThumbDrawable != null && mThumbDrawable.isStateful()) {
    176             mThumbDrawable.setState(state);
    177         }
    178         if (mTrackDrawable != null && mTrackDrawable.isStateful()) {
    179             mTrackDrawable.setState(state);
    180         }
    181     }
    182 
    183     public void setScrollbarPosition(int position) {
    184         mPosition = position;
    185         switch (position) {
    186             default:
    187             case View.SCROLLBAR_POSITION_DEFAULT:
    188             case View.SCROLLBAR_POSITION_RIGHT:
    189                 mOverlayDrawable = mOverlayDrawableRight;
    190                 break;
    191             case View.SCROLLBAR_POSITION_LEFT:
    192                 mOverlayDrawable = mOverlayDrawableLeft;
    193                 break;
    194         }
    195     }
    196 
    197     public int getWidth() {
    198         return mThumbW;
    199     }
    200 
    201     public void setState(int state) {
    202         switch (state) {
    203             case STATE_NONE:
    204                 mHandler.removeCallbacks(mScrollFade);
    205                 mList.invalidate();
    206                 break;
    207             case STATE_VISIBLE:
    208                 if (mState != STATE_VISIBLE) { // Optimization
    209                     resetThumbPos();
    210                 }
    211                 // Fall through
    212             case STATE_DRAGGING:
    213                 mHandler.removeCallbacks(mScrollFade);
    214                 break;
    215             case STATE_EXIT:
    216                 int viewWidth = mList.getWidth();
    217                 mList.invalidate(viewWidth - mThumbW, mThumbY, viewWidth, mThumbY + mThumbH);
    218                 break;
    219         }
    220         mState = state;
    221         refreshDrawableState();
    222     }
    223 
    224     public int getState() {
    225         return mState;
    226     }
    227 
    228     private void resetThumbPos() {
    229         final int viewWidth = mList.getWidth();
    230         // Bounds are always top right. Y coordinate get's translated during draw
    231         switch (mPosition) {
    232             case View.SCROLLBAR_POSITION_DEFAULT:
    233             case View.SCROLLBAR_POSITION_RIGHT:
    234                 mThumbDrawable.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH);
    235                 break;
    236             case View.SCROLLBAR_POSITION_LEFT:
    237                 mThumbDrawable.setBounds(0, 0, mThumbW, mThumbH);
    238                 break;
    239         }
    240         mThumbDrawable.setAlpha(ScrollFade.ALPHA_MAX);
    241     }
    242 
    243     private void useThumbDrawable(Context context, Drawable drawable) {
    244         mThumbDrawable = drawable;
    245         if (drawable instanceof NinePatchDrawable) {
    246             mThumbW = context.getResources().getDimensionPixelSize(
    247                     com.android.internal.R.dimen.fastscroll_thumb_width);
    248             mThumbH = context.getResources().getDimensionPixelSize(
    249                     com.android.internal.R.dimen.fastscroll_thumb_height);
    250         } else {
    251             mThumbW = drawable.getIntrinsicWidth();
    252             mThumbH = drawable.getIntrinsicHeight();
    253         }
    254         mChangedBounds = true;
    255     }
    256 
    257     private void init(Context context) {
    258         // Get both the scrollbar states drawables
    259         TypedArray ta = context.getTheme().obtainStyledAttributes(ATTRS);
    260         useThumbDrawable(context, ta.getDrawable(THUMB_DRAWABLE));
    261         mTrackDrawable = ta.getDrawable(TRACK_DRAWABLE);
    262 
    263         mOverlayDrawableLeft = ta.getDrawable(PREVIEW_BACKGROUND_LEFT);
    264         mOverlayDrawableRight = ta.getDrawable(PREVIEW_BACKGROUND_RIGHT);
    265         mOverlayPosition = ta.getInt(OVERLAY_POSITION, OVERLAY_FLOATING);
    266 
    267         mScrollCompleted = true;
    268 
    269         getSectionsFromIndexer();
    270 
    271         mOverlaySize = context.getResources().getDimensionPixelSize(
    272                 com.android.internal.R.dimen.fastscroll_overlay_size);
    273         mOverlayPos = new RectF();
    274         mScrollFade = new ScrollFade();
    275         mPaint = new Paint();
    276         mPaint.setAntiAlias(true);
    277         mPaint.setTextAlign(Paint.Align.CENTER);
    278         mPaint.setTextSize(mOverlaySize / 2);
    279 
    280         ColorStateList textColor = ta.getColorStateList(TEXT_COLOR);
    281         int textColorNormal = textColor.getDefaultColor();
    282         mPaint.setColor(textColorNormal);
    283         mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    284 
    285         // to show mOverlayDrawable properly
    286         if (mList.getWidth() > 0 && mList.getHeight() > 0) {
    287             onSizeChanged(mList.getWidth(), mList.getHeight(), 0, 0);
    288         }
    289 
    290         mState = STATE_NONE;
    291         refreshDrawableState();
    292 
    293         ta.recycle();
    294 
    295         mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    296 
    297         mMatchDragPosition = context.getApplicationInfo().targetSdkVersion >=
    298                 android.os.Build.VERSION_CODES.HONEYCOMB;
    299 
    300         setScrollbarPosition(mList.getVerticalScrollbarPosition());
    301     }
    302 
    303     void stop() {
    304         setState(STATE_NONE);
    305     }
    306 
    307     boolean isVisible() {
    308         return !(mState == STATE_NONE);
    309     }
    310 
    311     public void draw(Canvas canvas) {
    312 
    313         if (mState == STATE_NONE) {
    314             // No need to draw anything
    315             return;
    316         }
    317 
    318         final int y = mThumbY;
    319         final int viewWidth = mList.getWidth();
    320         final FastScroller.ScrollFade scrollFade = mScrollFade;
    321 
    322         int alpha = -1;
    323         if (mState == STATE_EXIT) {
    324             alpha = scrollFade.getAlpha();
    325             if (alpha < ScrollFade.ALPHA_MAX / 2) {
    326                 mThumbDrawable.setAlpha(alpha * 2);
    327             }
    328             int left = 0;
    329             switch (mPosition) {
    330                 case View.SCROLLBAR_POSITION_DEFAULT:
    331                 case View.SCROLLBAR_POSITION_RIGHT:
    332                     left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
    333                     break;
    334                 case View.SCROLLBAR_POSITION_LEFT:
    335                     left = -mThumbW + (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
    336                     break;
    337             }
    338             mThumbDrawable.setBounds(left, 0, left + mThumbW, mThumbH);
    339             mChangedBounds = true;
    340         }
    341 
    342         if (mTrackDrawable != null) {
    343             final Rect thumbBounds = mThumbDrawable.getBounds();
    344             final int left = thumbBounds.left;
    345             final int halfThumbHeight = (thumbBounds.bottom - thumbBounds.top) / 2;
    346             final int trackWidth = mTrackDrawable.getIntrinsicWidth();
    347             final int trackLeft = (left + mThumbW / 2) - trackWidth / 2;
    348             mTrackDrawable.setBounds(trackLeft, halfThumbHeight,
    349                     trackLeft + trackWidth, mList.getHeight() - halfThumbHeight);
    350             mTrackDrawable.draw(canvas);
    351         }
    352 
    353         canvas.translate(0, y);
    354         mThumbDrawable.draw(canvas);
    355         canvas.translate(0, -y);
    356 
    357         // If user is dragging the scroll bar, draw the alphabet overlay
    358         if (mState == STATE_DRAGGING && mDrawOverlay) {
    359             if (mOverlayPosition == OVERLAY_AT_THUMB) {
    360                 int left = 0;
    361                 switch (mPosition) {
    362                     default:
    363                     case View.SCROLLBAR_POSITION_DEFAULT:
    364                     case View.SCROLLBAR_POSITION_RIGHT:
    365                         left = Math.max(0,
    366                                 mThumbDrawable.getBounds().left - mThumbW - mOverlaySize);
    367                         break;
    368                     case View.SCROLLBAR_POSITION_LEFT:
    369                         left = Math.min(mThumbDrawable.getBounds().right + mThumbW,
    370                                 mList.getWidth() - mOverlaySize);
    371                         break;
    372                 }
    373 
    374                 int top = Math.max(0,
    375                         Math.min(y + (mThumbH - mOverlaySize) / 2, mList.getHeight() - mOverlaySize));
    376 
    377                 final RectF pos = mOverlayPos;
    378                 pos.left = left;
    379                 pos.right = pos.left + mOverlaySize;
    380                 pos.top = top;
    381                 pos.bottom = pos.top + mOverlaySize;
    382                 if (mOverlayDrawable != null) {
    383                     mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
    384                             (int) pos.right, (int) pos.bottom);
    385                 }
    386             }
    387             mOverlayDrawable.draw(canvas);
    388             final Paint paint = mPaint;
    389             float descent = paint.descent();
    390             final RectF rectF = mOverlayPos;
    391             final Rect tmpRect = mTmpRect;
    392             mOverlayDrawable.getPadding(tmpRect);
    393             final int hOff = (tmpRect.right - tmpRect.left) / 2;
    394             final int vOff = (tmpRect.bottom - tmpRect.top) / 2;
    395             canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2 - hOff,
    396                     (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent - vOff,
    397                     paint);
    398         } else if (mState == STATE_EXIT) {
    399             if (alpha == 0) { // Done with exit
    400                 setState(STATE_NONE);
    401             } else if (mTrackDrawable != null) {
    402                 mList.invalidate(viewWidth - mThumbW, 0, viewWidth, mList.getHeight());
    403             } else {
    404                 mList.invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);
    405             }
    406         }
    407     }
    408 
    409     void onSizeChanged(int w, int h, int oldw, int oldh) {
    410         if (mThumbDrawable != null) {
    411             switch (mPosition) {
    412                 default:
    413                 case View.SCROLLBAR_POSITION_DEFAULT:
    414                 case View.SCROLLBAR_POSITION_RIGHT:
    415                     mThumbDrawable.setBounds(w - mThumbW, 0, w, mThumbH);
    416                     break;
    417                 case View.SCROLLBAR_POSITION_LEFT:
    418                     mThumbDrawable.setBounds(0, 0, mThumbW, mThumbH);
    419                     break;
    420             }
    421         }
    422         if (mOverlayPosition == OVERLAY_FLOATING) {
    423             final RectF pos = mOverlayPos;
    424             pos.left = (w - mOverlaySize) / 2;
    425             pos.right = pos.left + mOverlaySize;
    426             pos.top = h / 10; // 10% from top
    427             pos.bottom = pos.top + mOverlaySize;
    428             if (mOverlayDrawable != null) {
    429                 mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
    430                         (int) pos.right, (int) pos.bottom);
    431             }
    432         }
    433     }
    434 
    435     void onItemCountChanged(int oldCount, int newCount) {
    436         if (mAlwaysShow) {
    437             mLongList = true;
    438         }
    439     }
    440 
    441     void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    442             int totalItemCount) {
    443         // Are there enough pages to require fast scroll? Recompute only if total count changes
    444         if (mItemCount != totalItemCount && visibleItemCount > 0) {
    445             mItemCount = totalItemCount;
    446             mLongList = mItemCount / visibleItemCount >= MIN_PAGES;
    447         }
    448         if (mAlwaysShow) {
    449             mLongList = true;
    450         }
    451         if (!mLongList) {
    452             if (mState != STATE_NONE) {
    453                 setState(STATE_NONE);
    454             }
    455             return;
    456         }
    457         if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING) {
    458             mThumbY = getThumbPositionForListPosition(firstVisibleItem, visibleItemCount,
    459                     totalItemCount);
    460             if (mChangedBounds) {
    461                 resetThumbPos();
    462                 mChangedBounds = false;
    463             }
    464         }
    465         mScrollCompleted = true;
    466         if (firstVisibleItem == mVisibleItem) {
    467             return;
    468         }
    469         mVisibleItem = firstVisibleItem;
    470         if (mState != STATE_DRAGGING) {
    471             setState(STATE_VISIBLE);
    472             if (!mAlwaysShow) {
    473                 mHandler.postDelayed(mScrollFade, FADE_TIMEOUT);
    474             }
    475         }
    476     }
    477 
    478     SectionIndexer getSectionIndexer() {
    479         return mSectionIndexer;
    480     }
    481 
    482     Object[] getSections() {
    483         if (mListAdapter == null && mList != null) {
    484             getSectionsFromIndexer();
    485         }
    486         return mSections;
    487     }
    488 
    489     void getSectionsFromIndexer() {
    490         Adapter adapter = mList.getAdapter();
    491         mSectionIndexer = null;
    492         if (adapter instanceof HeaderViewListAdapter) {
    493             mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
    494             adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
    495         }
    496         if (adapter instanceof ExpandableListConnector) {
    497             ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter();
    498             if (expAdapter instanceof SectionIndexer) {
    499                 mSectionIndexer = (SectionIndexer) expAdapter;
    500                 mListAdapter = (BaseAdapter) adapter;
    501                 mSections = mSectionIndexer.getSections();
    502             }
    503         } else {
    504             if (adapter instanceof SectionIndexer) {
    505                 mListAdapter = (BaseAdapter) adapter;
    506                 mSectionIndexer = (SectionIndexer) adapter;
    507                 mSections = mSectionIndexer.getSections();
    508                 if (mSections == null) {
    509                     mSections = new String[] { " " };
    510                 }
    511             } else {
    512                 mListAdapter = (BaseAdapter) adapter;
    513                 mSections = new String[] { " " };
    514             }
    515         }
    516     }
    517 
    518     public void onSectionsChanged() {
    519         mListAdapter = null;
    520     }
    521 
    522     void scrollTo(float position) {
    523         int count = mList.getCount();
    524         mScrollCompleted = false;
    525         float fThreshold = (1.0f / count) / 8;
    526         final Object[] sections = mSections;
    527         int sectionIndex;
    528         if (sections != null && sections.length > 1) {
    529             final int nSections = sections.length;
    530             int section = (int) (position * nSections);
    531             if (section >= nSections) {
    532                 section = nSections - 1;
    533             }
    534             int exactSection = section;
    535             sectionIndex = section;
    536             int index = mSectionIndexer.getPositionForSection(section);
    537             // Given the expected section and index, the following code will
    538             // try to account for missing sections (no names starting with..)
    539             // It will compute the scroll space of surrounding empty sections
    540             // and interpolate the currently visible letter's range across the
    541             // available space, so that there is always some list movement while
    542             // the user moves the thumb.
    543             int nextIndex = count;
    544             int prevIndex = index;
    545             int prevSection = section;
    546             int nextSection = section + 1;
    547             // Assume the next section is unique
    548             if (section < nSections - 1) {
    549                 nextIndex = mSectionIndexer.getPositionForSection(section + 1);
    550             }
    551 
    552             // Find the previous index if we're slicing the previous section
    553             if (nextIndex == index) {
    554                 // Non-existent letter
    555                 while (section > 0) {
    556                     section--;
    557                     prevIndex = mSectionIndexer.getPositionForSection(section);
    558                     if (prevIndex != index) {
    559                         prevSection = section;
    560                         sectionIndex = section;
    561                         break;
    562                     } else if (section == 0) {
    563                         // When section reaches 0 here, sectionIndex must follow it.
    564                         // Assuming mSectionIndexer.getPositionForSection(0) == 0.
    565                         sectionIndex = 0;
    566                         break;
    567                     }
    568                 }
    569             }
    570             // Find the next index, in case the assumed next index is not
    571             // unique. For instance, if there is no P, then request for P's
    572             // position actually returns Q's. So we need to look ahead to make
    573             // sure that there is really a Q at Q's position. If not, move
    574             // further down...
    575             int nextNextSection = nextSection + 1;
    576             while (nextNextSection < nSections &&
    577                     mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
    578                 nextNextSection++;
    579                 nextSection++;
    580             }
    581             // Compute the beginning and ending scroll range percentage of the
    582             // currently visible letter. This could be equal to or greater than
    583             // (1 / nSections).
    584             float fPrev = (float) prevSection / nSections;
    585             float fNext = (float) nextSection / nSections;
    586             if (prevSection == exactSection && position - fPrev < fThreshold) {
    587                 index = prevIndex;
    588             } else {
    589                 index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev)
    590                     / (fNext - fPrev));
    591             }
    592             // Don't overflow
    593             if (index > count - 1) index = count - 1;
    594 
    595             if (mList instanceof ExpandableListView) {
    596                 ExpandableListView expList = (ExpandableListView) mList;
    597                 expList.setSelectionFromTop(expList.getFlatListPosition(
    598                         ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
    599             } else if (mList instanceof ListView) {
    600                 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
    601             } else {
    602                 mList.setSelection(index + mListOffset);
    603             }
    604         } else {
    605             int index = (int) (position * count);
    606             // Don't overflow
    607             if (index > count - 1) index = count - 1;
    608 
    609             if (mList instanceof ExpandableListView) {
    610                 ExpandableListView expList = (ExpandableListView) mList;
    611                 expList.setSelectionFromTop(expList.getFlatListPosition(
    612                         ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
    613             } else if (mList instanceof ListView) {
    614                 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
    615             } else {
    616                 mList.setSelection(index + mListOffset);
    617             }
    618             sectionIndex = -1;
    619         }
    620 
    621         if (sectionIndex >= 0) {
    622             String text = mSectionText = sections[sectionIndex].toString();
    623             mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') &&
    624                     sectionIndex < sections.length;
    625         } else {
    626             mDrawOverlay = false;
    627         }
    628     }
    629 
    630     private int getThumbPositionForListPosition(int firstVisibleItem, int visibleItemCount,
    631             int totalItemCount) {
    632         if (mSectionIndexer == null || mListAdapter == null) {
    633             getSectionsFromIndexer();
    634         }
    635         if (mSectionIndexer == null || !mMatchDragPosition) {
    636             return ((mList.getHeight() - mThumbH) * firstVisibleItem)
    637                     / (totalItemCount - visibleItemCount);
    638         }
    639 
    640         firstVisibleItem -= mListOffset;
    641         if (firstVisibleItem < 0) {
    642             return 0;
    643         }
    644         totalItemCount -= mListOffset;
    645 
    646         final int trackHeight = mList.getHeight() - mThumbH;
    647 
    648         final int section = mSectionIndexer.getSectionForPosition(firstVisibleItem);
    649         final int sectionPos = mSectionIndexer.getPositionForSection(section);
    650         final int nextSectionPos = mSectionIndexer.getPositionForSection(section + 1);
    651         final int sectionCount = mSections.length;
    652         final int positionsInSection = nextSectionPos - sectionPos;
    653 
    654         final View child = mList.getChildAt(0);
    655         final float incrementalPos = child == null ? 0 : firstVisibleItem +
    656                 (float) (mList.getPaddingTop() - child.getTop()) / child.getHeight();
    657         final float posWithinSection = (incrementalPos - sectionPos) / positionsInSection;
    658         int result = (int) ((section + posWithinSection) / sectionCount * trackHeight);
    659 
    660         // Fake out the scrollbar for the last item. Since the section indexer won't
    661         // ever actually move the list in this end space, make scrolling across the last item
    662         // account for whatever space is remaining.
    663         if (firstVisibleItem > 0 && firstVisibleItem + visibleItemCount == totalItemCount) {
    664             final View lastChild = mList.getChildAt(visibleItemCount - 1);
    665             final float lastItemVisible = (float) (mList.getHeight() - mList.getPaddingBottom()
    666                     - lastChild.getTop()) / lastChild.getHeight();
    667             result += (trackHeight - result) * lastItemVisible;
    668         }
    669 
    670         return result;
    671     }
    672 
    673     private void cancelFling() {
    674         // Cancel the list fling
    675         MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
    676         mList.onTouchEvent(cancelFling);
    677         cancelFling.recycle();
    678     }
    679 
    680     void cancelPendingDrag() {
    681         mList.removeCallbacks(mDeferStartDrag);
    682         mPendingDrag = false;
    683     }
    684 
    685     void startPendingDrag() {
    686         mPendingDrag = true;
    687         mList.postDelayed(mDeferStartDrag, PENDING_DRAG_DELAY);
    688     }
    689 
    690     void beginDrag() {
    691         setState(STATE_DRAGGING);
    692         if (mListAdapter == null && mList != null) {
    693             getSectionsFromIndexer();
    694         }
    695         if (mList != null) {
    696             mList.requestDisallowInterceptTouchEvent(true);
    697             mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
    698         }
    699 
    700         cancelFling();
    701     }
    702 
    703     boolean onInterceptTouchEvent(MotionEvent ev) {
    704         switch (ev.getActionMasked()) {
    705             case MotionEvent.ACTION_DOWN:
    706                 if (mState > STATE_NONE && isPointInside(ev.getX(), ev.getY())) {
    707                     if (!mList.isInScrollingContainer()) {
    708                         beginDrag();
    709                         return true;
    710                     }
    711                     mInitialTouchY = ev.getY();
    712                     startPendingDrag();
    713                 }
    714                 break;
    715             case MotionEvent.ACTION_UP:
    716             case MotionEvent.ACTION_CANCEL:
    717                 cancelPendingDrag();
    718                 break;
    719         }
    720         return false;
    721     }
    722 
    723     boolean onTouchEvent(MotionEvent me) {
    724         if (mState == STATE_NONE) {
    725             return false;
    726         }
    727 
    728         final int action = me.getAction();
    729 
    730         if (action == MotionEvent.ACTION_DOWN) {
    731             if (isPointInside(me.getX(), me.getY())) {
    732                 if (!mList.isInScrollingContainer()) {
    733                     beginDrag();
    734                     return true;
    735                 }
    736                 mInitialTouchY = me.getY();
    737                 startPendingDrag();
    738             }
    739         } else if (action == MotionEvent.ACTION_UP) { // don't add ACTION_CANCEL here
    740             if (mPendingDrag) {
    741                 // Allow a tap to scroll.
    742                 beginDrag();
    743 
    744                 final int viewHeight = mList.getHeight();
    745                 // Jitter
    746                 int newThumbY = (int) me.getY() - mThumbH + 10;
    747                 if (newThumbY < 0) {
    748                     newThumbY = 0;
    749                 } else if (newThumbY + mThumbH > viewHeight) {
    750                     newThumbY = viewHeight - mThumbH;
    751                 }
    752                 mThumbY = newThumbY;
    753                 scrollTo((float) mThumbY / (viewHeight - mThumbH));
    754 
    755                 cancelPendingDrag();
    756                 // Will hit the STATE_DRAGGING check below
    757             }
    758             if (mState == STATE_DRAGGING) {
    759                 if (mList != null) {
    760                     // ViewGroup does the right thing already, but there might
    761                     // be other classes that don't properly reset on touch-up,
    762                     // so do this explicitly just in case.
    763                     mList.requestDisallowInterceptTouchEvent(false);
    764                     mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
    765                 }
    766                 setState(STATE_VISIBLE);
    767                 final Handler handler = mHandler;
    768                 handler.removeCallbacks(mScrollFade);
    769                 if (!mAlwaysShow) {
    770                     handler.postDelayed(mScrollFade, 1000);
    771                 }
    772 
    773                 mList.invalidate();
    774                 return true;
    775             }
    776         } else if (action == MotionEvent.ACTION_MOVE) {
    777             if (mPendingDrag) {
    778                 final float y = me.getY();
    779                 if (Math.abs(y - mInitialTouchY) > mScaledTouchSlop) {
    780                     setState(STATE_DRAGGING);
    781                     if (mListAdapter == null && mList != null) {
    782                         getSectionsFromIndexer();
    783                     }
    784                     if (mList != null) {
    785                         mList.requestDisallowInterceptTouchEvent(true);
    786                         mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
    787                     }
    788 
    789                     cancelFling();
    790                     cancelPendingDrag();
    791                     // Will hit the STATE_DRAGGING check below
    792                 }
    793             }
    794             if (mState == STATE_DRAGGING) {
    795                 final int viewHeight = mList.getHeight();
    796                 // Jitter
    797                 int newThumbY = (int) me.getY() - mThumbH + 10;
    798                 if (newThumbY < 0) {
    799                     newThumbY = 0;
    800                 } else if (newThumbY + mThumbH > viewHeight) {
    801                     newThumbY = viewHeight - mThumbH;
    802                 }
    803                 if (Math.abs(mThumbY - newThumbY) < 2) {
    804                     return true;
    805                 }
    806                 mThumbY = newThumbY;
    807                 // If the previous scrollTo is still pending
    808                 if (mScrollCompleted) {
    809                     scrollTo((float) mThumbY / (viewHeight - mThumbH));
    810                 }
    811                 return true;
    812             }
    813         } else if (action == MotionEvent.ACTION_CANCEL) {
    814             cancelPendingDrag();
    815         }
    816         return false;
    817     }
    818 
    819     boolean isPointInside(float x, float y) {
    820         boolean inTrack = false;
    821         switch (mPosition) {
    822             default:
    823             case View.SCROLLBAR_POSITION_DEFAULT:
    824             case View.SCROLLBAR_POSITION_RIGHT:
    825                 inTrack = x > mList.getWidth() - mThumbW;
    826                 break;
    827             case View.SCROLLBAR_POSITION_LEFT:
    828                 inTrack = x < mThumbW;
    829                 break;
    830         }
    831 
    832         // Allow taps in the track to start moving.
    833         return inTrack && (mTrackDrawable != null || y >= mThumbY && y <= mThumbY + mThumbH);
    834     }
    835 
    836     public class ScrollFade implements Runnable {
    837 
    838         long mStartTime;
    839         long mFadeDuration;
    840         static final int ALPHA_MAX = 208;
    841         static final long FADE_DURATION = 200;
    842 
    843         void startFade() {
    844             mFadeDuration = FADE_DURATION;
    845             mStartTime = SystemClock.uptimeMillis();
    846             setState(STATE_EXIT);
    847         }
    848 
    849         int getAlpha() {
    850             if (getState() != STATE_EXIT) {
    851                 return ALPHA_MAX;
    852             }
    853             int alpha;
    854             long now = SystemClock.uptimeMillis();
    855             if (now > mStartTime + mFadeDuration) {
    856                 alpha = 0;
    857             } else {
    858                 alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration);
    859             }
    860             return alpha;
    861         }
    862 
    863         public void run() {
    864             if (getState() != STATE_EXIT) {
    865                 startFade();
    866                 return;
    867             }
    868 
    869             if (getAlpha() > 0) {
    870                 mList.invalidate();
    871             } else {
    872                 setState(STATE_NONE);
    873             }
    874         }
    875     }
    876 }
    877