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