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