• 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         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 
getWidth()200     public int getWidth() {
201         return mThumbW;
202     }
203 
setState(int state)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 
getState()227     public int getState() {
228         return mState;
229     }
230 
resetThumbPos()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 
useThumbDrawable(Context context, Drawable drawable)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 
init(Context context)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 
stop()305     void stop() {
306         setState(STATE_NONE);
307     }
308 
isVisible()309     boolean isVisible() {
310         return !(mState == STATE_NONE);
311     }
312 
draw(Canvas canvas)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 
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_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 
onItemCountChanged(int oldCount, int newCount)434     void onItemCountChanged(int oldCount, int newCount) {
435         if (mAlwaysShow) {
436             mLongList = true;
437         }
438     }
439 
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)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 
getSectionIndexer()477     SectionIndexer getSectionIndexer() {
478         return mSectionIndexer;
479     }
480 
getSections()481     Object[] getSections() {
482         if (mListAdapter == null && mList != null) {
483             getSectionsFromIndexer();
484         }
485         return mSections;
486     }
487 
getSectionsFromIndexer()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 
onSectionsChanged()517     public void onSectionsChanged() {
518         mListAdapter = null;
519     }
520 
scrollTo(float position)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 
onTouchEvent(MotionEvent me)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 
isPointInside(float x, float y)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 
startFade()841         void startFade() {
842             mFadeDuration = FADE_DURATION;
843             mStartTime = SystemClock.uptimeMillis();
844             setState(STATE_EXIT);
845         }
846 
getAlpha()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 
run()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