• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.widget;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.graphics.Paint;
24 import android.graphics.Rect;
25 import android.graphics.RectF;
26 import android.graphics.drawable.Drawable;
27 import android.graphics.drawable.NinePatchDrawable;
28 import android.os.Handler;
29 import android.os.SystemClock;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.ViewConfiguration;
33 import android.widget.AbsListView.OnScrollListener;
34 
35 /**
36  * Helper class for AbsListView to draw and control the Fast Scroll thumb
37  */
38 class 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 
FastScroller(Context context, AbsListView listView)153     public FastScroller(Context context, AbsListView listView) {
154         mList = listView;
155         init(context);
156     }
157 
setAlwaysShow(boolean alwaysShow)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 
isAlwaysShowEnabled()168     public boolean isAlwaysShowEnabled() {
169         return mAlwaysShow;
170     }
171 
refreshDrawableState()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 
setScrollbarPosition(int position)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 
getWidth()197     public int getWidth() {
198         return mThumbW;
199     }
200 
setState(int state)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 
getState()224     public int getState() {
225         return mState;
226     }
227 
resetThumbPos()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 
useThumbDrawable(Context context, Drawable drawable)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 
init(Context context)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 
stop()303     void stop() {
304         setState(STATE_NONE);
305     }
306 
isVisible()307     boolean isVisible() {
308         return !(mState == STATE_NONE);
309     }
310 
draw(Canvas canvas)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 
onSizeChanged(int w, int h, int oldw, int oldh)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 
onItemCountChanged(int oldCount, int newCount)435     void onItemCountChanged(int oldCount, int newCount) {
436         if (mAlwaysShow) {
437             mLongList = true;
438         }
439     }
440 
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)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 
getSectionIndexer()478     SectionIndexer getSectionIndexer() {
479         return mSectionIndexer;
480     }
481 
getSections()482     Object[] getSections() {
483         if (mListAdapter == null && mList != null) {
484             getSectionsFromIndexer();
485         }
486         return mSections;
487     }
488 
getSectionsFromIndexer()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 
onSectionsChanged()518     public void onSectionsChanged() {
519         mListAdapter = null;
520     }
521 
scrollTo(float position)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 
onTouchEvent(MotionEvent me)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 
isPointInside(float x, float y)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 
startFade()843         void startFade() {
844             mFadeDuration = FADE_DURATION;
845             mStartTime = SystemClock.uptimeMillis();
846             setState(STATE_EXIT);
847         }
848 
getAlpha()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 
run()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