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