FastScroller.java revision 20232d0f7ce2220df72dd78bed052f6b4a643f10
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        com.android.internal.R.attr.fastScrollThumbDrawable,
62        com.android.internal.R.attr.fastScrollTrackDrawable,
63        com.android.internal.R.attr.fastScrollPreviewBackgroundLeft,
64        com.android.internal.R.attr.fastScrollPreviewBackgroundRight,
65        com.android.internal.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        final Resources res = context.getResources();
231        TypedArray ta = context.getTheme().obtainStyledAttributes(ATTRS);
232        useThumbDrawable(context, ta.getDrawable(ta.getIndex(THUMB_DRAWABLE)));
233        mTrackDrawable = ta.getDrawable(ta.getIndex(TRACK_DRAWABLE));
234
235        mOverlayDrawableLeft = ta.getDrawable(ta.getIndex(PREVIEW_BACKGROUND_LEFT));
236        mOverlayDrawableRight = ta.getDrawable(ta.getIndex(PREVIEW_BACKGROUND_RIGHT));
237        mOverlayPosition = ta.getInt(ta.getIndex(OVERLAY_POSITION), OVERLAY_FLOATING);
238
239        mScrollCompleted = true;
240
241        getSectionsFromIndexer();
242
243        mOverlaySize = context.getResources().getDimensionPixelSize(
244                com.android.internal.R.dimen.fastscroll_overlay_size);
245        mOverlayPos = new RectF();
246        mScrollFade = new ScrollFade();
247        mPaint = new Paint();
248        mPaint.setAntiAlias(true);
249        mPaint.setTextAlign(Paint.Align.CENTER);
250        mPaint.setTextSize(mOverlaySize / 2);
251
252        ColorStateList textColor = ta.getColorStateList(ta.getIndex(PRIMARY_TEXT_COLOR));
253        int textColorNormal = textColor.getDefaultColor();
254        mPaint.setColor(textColorNormal);
255        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
256
257        // to show mOverlayDrawable properly
258        if (mList.getWidth() > 0 && mList.getHeight() > 0) {
259            onSizeChanged(mList.getWidth(), mList.getHeight(), 0, 0);
260        }
261
262        mState = STATE_NONE;
263        refreshDrawableState();
264
265        ta.recycle();
266
267        setScrollbarPosition(mList.getVerticalScrollbarPosition());
268    }
269
270    void stop() {
271        setState(STATE_NONE);
272    }
273
274    boolean isVisible() {
275        return !(mState == STATE_NONE);
276    }
277
278    public void draw(Canvas canvas) {
279
280        if (mState == STATE_NONE) {
281            // No need to draw anything
282            return;
283        }
284
285        final int y = mThumbY;
286        final int viewWidth = mList.getWidth();
287        final FastScroller.ScrollFade scrollFade = mScrollFade;
288
289        int alpha = -1;
290        if (mState == STATE_EXIT) {
291            alpha = scrollFade.getAlpha();
292            if (alpha < ScrollFade.ALPHA_MAX / 2) {
293                mThumbDrawable.setAlpha(alpha * 2);
294            }
295            int left = 0;
296            switch (mPosition) {
297                case View.SCROLLBAR_POSITION_DEFAULT:
298                case View.SCROLLBAR_POSITION_RIGHT:
299                    left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
300                    break;
301                case View.SCROLLBAR_POSITION_LEFT:
302                    left = -mThumbW + (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
303                    break;
304            }
305            mThumbDrawable.setBounds(left, 0, left + mThumbW, mThumbH);
306            mChangedBounds = true;
307        }
308
309        if (mTrackDrawable != null) {
310            final int left = mThumbDrawable.getBounds().left;
311            final int trackWidth = mTrackDrawable.getIntrinsicWidth();
312            final int trackLeft = (left + mThumbW) / 2 - trackWidth / 2;
313            mTrackDrawable.setBounds(trackLeft, 0, trackLeft + trackWidth, mList.getHeight());
314            mTrackDrawable.draw(canvas);
315        }
316
317        canvas.translate(0, y);
318        mThumbDrawable.draw(canvas);
319        canvas.translate(0, -y);
320
321        // If user is dragging the scroll bar, draw the alphabet overlay
322        if (mState == STATE_DRAGGING && mDrawOverlay) {
323            if (mOverlayPosition == OVERLAY_AT_THUMB) {
324                int left = 0;
325                switch (mPosition) {
326                    default:
327                    case View.SCROLLBAR_POSITION_DEFAULT:
328                    case View.SCROLLBAR_POSITION_RIGHT:
329                        left = Math.max(0,
330                                mThumbDrawable.getBounds().left - mThumbW - mOverlaySize);
331                        break;
332                    case View.SCROLLBAR_POSITION_LEFT:
333                        left = Math.min(mThumbDrawable.getBounds().right + mThumbW,
334                                mList.getWidth() - mOverlaySize);
335                        break;
336                }
337
338                int top = Math.max(0,
339                        Math.min(y + (mThumbH - mOverlaySize) / 2, mList.getHeight() - mOverlaySize));
340
341                final RectF pos = mOverlayPos;
342                pos.left = left;
343                pos.right = pos.left + mOverlaySize;
344                pos.top = top;
345                pos.bottom = pos.top + mOverlaySize;
346                if (mOverlayDrawable != null) {
347                    mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
348                            (int) pos.right, (int) pos.bottom);
349                }
350            }
351            mOverlayDrawable.draw(canvas);
352            final Paint paint = mPaint;
353            float descent = paint.descent();
354            final RectF rectF = mOverlayPos;
355            final Rect tmpRect = mTmpRect;
356            mOverlayDrawable.getPadding(tmpRect);
357            final int hOff = (tmpRect.right - tmpRect.left) / 2;
358            final int vOff = (tmpRect.bottom - tmpRect.top) / 2;
359            canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2 - hOff,
360                    (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent - vOff,
361                    paint);
362        } else if (mState == STATE_EXIT) {
363            if (alpha == 0) { // Done with exit
364                setState(STATE_NONE);
365            } else {
366                mList.invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);
367            }
368        }
369    }
370
371    void onSizeChanged(int w, int h, int oldw, int oldh) {
372        if (mThumbDrawable != null) {
373            switch (mPosition) {
374                default:
375                case View.SCROLLBAR_POSITION_DEFAULT:
376                case View.SCROLLBAR_POSITION_RIGHT:
377                    mThumbDrawable.setBounds(w - mThumbW, 0, w, mThumbH);
378                    break;
379                case View.SCROLLBAR_POSITION_LEFT:
380                    mThumbDrawable.setBounds(0, 0, mThumbW, mThumbH);
381                    break;
382            }
383        }
384        if (mOverlayPosition == OVERLAY_FLOATING) {
385            final RectF pos = mOverlayPos;
386            pos.left = (w - mOverlaySize) / 2;
387            pos.right = pos.left + mOverlaySize;
388            pos.top = h / 10; // 10% from top
389            pos.bottom = pos.top + mOverlaySize;
390            if (mOverlayDrawable != null) {
391                mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
392                        (int) pos.right, (int) pos.bottom);
393            }
394        }
395    }
396
397    void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
398            int totalItemCount) {
399        // Are there enough pages to require fast scroll? Recompute only if total count changes
400        if (mItemCount != totalItemCount && visibleItemCount > 0) {
401            mItemCount = totalItemCount;
402            mLongList = mItemCount / visibleItemCount >= MIN_PAGES;
403        }
404        if (!mLongList) {
405            if (mState != STATE_NONE) {
406                setState(STATE_NONE);
407            }
408            return;
409        }
410        if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING ) {
411            mThumbY = ((mList.getHeight() - mThumbH) * firstVisibleItem)
412                    / (totalItemCount - visibleItemCount);
413            if (mChangedBounds) {
414                resetThumbPos();
415                mChangedBounds = false;
416            }
417        }
418        mScrollCompleted = true;
419        if (firstVisibleItem == mVisibleItem) {
420            return;
421        }
422        mVisibleItem = firstVisibleItem;
423        if (mState != STATE_DRAGGING) {
424            setState(STATE_VISIBLE);
425            if (!mAlwaysShow) {
426                mHandler.postDelayed(mScrollFade, FADE_TIMEOUT);
427            }
428        }
429    }
430
431    SectionIndexer getSectionIndexer() {
432        return mSectionIndexer;
433    }
434
435    Object[] getSections() {
436        if (mListAdapter == null && mList != null) {
437            getSectionsFromIndexer();
438        }
439        return mSections;
440    }
441
442    private void getSectionsFromIndexer() {
443        Adapter adapter = mList.getAdapter();
444        mSectionIndexer = null;
445        if (adapter instanceof HeaderViewListAdapter) {
446            mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
447            adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
448        }
449        if (adapter instanceof ExpandableListConnector) {
450            ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter();
451            if (expAdapter instanceof SectionIndexer) {
452                mSectionIndexer = (SectionIndexer) expAdapter;
453                mListAdapter = (BaseAdapter) adapter;
454                mSections = mSectionIndexer.getSections();
455            }
456        } else {
457            if (adapter instanceof SectionIndexer) {
458                mListAdapter = (BaseAdapter) adapter;
459                mSectionIndexer = (SectionIndexer) adapter;
460                mSections = mSectionIndexer.getSections();
461
462            } else {
463                mListAdapter = (BaseAdapter) adapter;
464                mSections = new String[] { " " };
465            }
466        }
467    }
468
469    private void scrollTo(float position) {
470        int count = mList.getCount();
471        mScrollCompleted = false;
472        float fThreshold = (1.0f / count) / 8;
473        final Object[] sections = mSections;
474        int sectionIndex;
475        if (sections != null && sections.length > 1) {
476            final int nSections = sections.length;
477            int section = (int) (position * nSections);
478            if (section >= nSections) {
479                section = nSections - 1;
480            }
481            int exactSection = section;
482            sectionIndex = section;
483            int index = mSectionIndexer.getPositionForSection(section);
484            // Given the expected section and index, the following code will
485            // try to account for missing sections (no names starting with..)
486            // It will compute the scroll space of surrounding empty sections
487            // and interpolate the currently visible letter's range across the
488            // available space, so that there is always some list movement while
489            // the user moves the thumb.
490            int nextIndex = count;
491            int prevIndex = index;
492            int prevSection = section;
493            int nextSection = section + 1;
494            // Assume the next section is unique
495            if (section < nSections - 1) {
496                nextIndex = mSectionIndexer.getPositionForSection(section + 1);
497            }
498
499            // Find the previous index if we're slicing the previous section
500            if (nextIndex == index) {
501                // Non-existent letter
502                while (section > 0) {
503                    section--;
504                    prevIndex = mSectionIndexer.getPositionForSection(section);
505                    if (prevIndex != index) {
506                        prevSection = section;
507                        sectionIndex = section;
508                        break;
509                    } else if (section == 0) {
510                        // When section reaches 0 here, sectionIndex must follow it.
511                        // Assuming mSectionIndexer.getPositionForSection(0) == 0.
512                        sectionIndex = 0;
513                        break;
514                    }
515                }
516            }
517            // Find the next index, in case the assumed next index is not
518            // unique. For instance, if there is no P, then request for P's
519            // position actually returns Q's. So we need to look ahead to make
520            // sure that there is really a Q at Q's position. If not, move
521            // further down...
522            int nextNextSection = nextSection + 1;
523            while (nextNextSection < nSections &&
524                    mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
525                nextNextSection++;
526                nextSection++;
527            }
528            // Compute the beginning and ending scroll range percentage of the
529            // currently visible letter. This could be equal to or greater than
530            // (1 / nSections).
531            float fPrev = (float) prevSection / nSections;
532            float fNext = (float) nextSection / nSections;
533            if (prevSection == exactSection && position - fPrev < fThreshold) {
534                index = prevIndex;
535            } else {
536                index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev)
537                    / (fNext - fPrev));
538            }
539            // Don't overflow
540            if (index > count - 1) index = count - 1;
541
542            if (mList instanceof ExpandableListView) {
543                ExpandableListView expList = (ExpandableListView) mList;
544                expList.setSelectionFromTop(expList.getFlatListPosition(
545                        ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
546            } else if (mList instanceof ListView) {
547                ((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
548            } else {
549                mList.setSelection(index + mListOffset);
550            }
551        } else {
552            int index = (int) (position * count);
553            if (mList instanceof ExpandableListView) {
554                ExpandableListView expList = (ExpandableListView) mList;
555                expList.setSelectionFromTop(expList.getFlatListPosition(
556                        ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
557            } else if (mList instanceof ListView) {
558                ((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
559            } else {
560                mList.setSelection(index + mListOffset);
561            }
562            sectionIndex = -1;
563        }
564
565        if (sectionIndex >= 0) {
566            String text = mSectionText = sections[sectionIndex].toString();
567            mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') &&
568                    sectionIndex < sections.length;
569        } else {
570            mDrawOverlay = false;
571        }
572    }
573
574    private void cancelFling() {
575        // Cancel the list fling
576        MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
577        mList.onTouchEvent(cancelFling);
578        cancelFling.recycle();
579    }
580
581    boolean onInterceptTouchEvent(MotionEvent ev) {
582        if (mState > STATE_NONE && ev.getAction() == MotionEvent.ACTION_DOWN) {
583            if (isPointInside(ev.getX(), ev.getY())) {
584                setState(STATE_DRAGGING);
585                return true;
586            }
587        }
588        return false;
589    }
590
591    boolean onTouchEvent(MotionEvent me) {
592        if (mState == STATE_NONE) {
593            return false;
594        }
595
596        final int action = me.getAction();
597
598        if (action == MotionEvent.ACTION_DOWN) {
599            if (isPointInside(me.getX(), me.getY())) {
600                setState(STATE_DRAGGING);
601                if (mListAdapter == null && mList != null) {
602                    getSectionsFromIndexer();
603                }
604                if (mList != null) {
605                    mList.requestDisallowInterceptTouchEvent(true);
606                    mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
607                }
608
609                cancelFling();
610                return true;
611            }
612        } else if (action == MotionEvent.ACTION_UP) { // don't add ACTION_CANCEL here
613            if (mState == STATE_DRAGGING) {
614                if (mList != null) {
615                    // ViewGroup does the right thing already, but there might
616                    // be other classes that don't properly reset on touch-up,
617                    // so do this explicitly just in case.
618                    mList.requestDisallowInterceptTouchEvent(false);
619                    mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
620                }
621                setState(STATE_VISIBLE);
622                final Handler handler = mHandler;
623                handler.removeCallbacks(mScrollFade);
624                if (!mAlwaysShow) {
625                    handler.postDelayed(mScrollFade, 1000);
626                }
627
628                mList.invalidate();
629                return true;
630            }
631        } else if (action == MotionEvent.ACTION_MOVE) {
632            if (mState == STATE_DRAGGING) {
633                final int viewHeight = mList.getHeight();
634                // Jitter
635                int newThumbY = (int) me.getY() - mThumbH + 10;
636                if (newThumbY < 0) {
637                    newThumbY = 0;
638                } else if (newThumbY + mThumbH > viewHeight) {
639                    newThumbY = viewHeight - mThumbH;
640                }
641                if (Math.abs(mThumbY - newThumbY) < 2) {
642                    return true;
643                }
644                mThumbY = newThumbY;
645                // If the previous scrollTo is still pending
646                if (mScrollCompleted) {
647                    scrollTo((float) mThumbY / (viewHeight - mThumbH));
648                }
649                return true;
650            }
651        }
652        return false;
653    }
654
655    boolean isPointInside(float x, float y) {
656        boolean inTrack = false;
657        switch (mPosition) {
658            default:
659            case View.SCROLLBAR_POSITION_DEFAULT:
660            case View.SCROLLBAR_POSITION_RIGHT:
661                inTrack = x > mList.getWidth() - mThumbW;
662                break;
663            case View.SCROLLBAR_POSITION_LEFT:
664                inTrack = x < mThumbW;
665                break;
666        }
667
668        // Allow taps in the track to start moving.
669        return inTrack && (mTrackDrawable != null || y >= mThumbY && y <= mThumbY + mThumbH);
670    }
671
672    public class ScrollFade implements Runnable {
673
674        long mStartTime;
675        long mFadeDuration;
676        static final int ALPHA_MAX = 208;
677        static final long FADE_DURATION = 200;
678
679        void startFade() {
680            mFadeDuration = FADE_DURATION;
681            mStartTime = SystemClock.uptimeMillis();
682            setState(STATE_EXIT);
683        }
684
685        int getAlpha() {
686            if (getState() != STATE_EXIT) {
687                return ALPHA_MAX;
688            }
689            int alpha;
690            long now = SystemClock.uptimeMillis();
691            if (now > mStartTime + mFadeDuration) {
692                alpha = 0;
693            } else {
694                alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration);
695            }
696            return alpha;
697        }
698
699        public void run() {
700            if (getState() != STATE_EXIT) {
701                startFade();
702                return;
703            }
704
705            if (getAlpha() > 0) {
706                mList.invalidate();
707            } else {
708                setState(STATE_NONE);
709            }
710        }
711    }
712}
713