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