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