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.view.ViewConfiguration;
33import android.widget.AbsListView.OnScrollListener;
34
35/**
36 * Helper class for AbsListView to draw and control the Fast Scroll thumb
37 */
38class 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