• 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.Resources;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Paint;
25 import android.graphics.RectF;
26 import android.graphics.drawable.Drawable;
27 import android.os.Handler;
28 import android.os.SystemClock;
29 import android.util.TypedValue;
30 import android.view.MotionEvent;
31 
32 /**
33  * Helper class for AbsListView to draw and control the Fast Scroll thumb
34  */
35 class FastScroller {
36 
37     // Minimum number of pages to justify showing a fast scroll thumb
38     private static int MIN_PAGES = 4;
39     // Scroll thumb not showing
40     private static final int STATE_NONE = 0;
41     // Not implemented yet - fade-in transition
42     private static final int STATE_ENTER = 1;
43     // Scroll thumb visible and moving along with the scrollbar
44     private static final int STATE_VISIBLE = 2;
45     // Scroll thumb being dragged by user
46     private static final int STATE_DRAGGING = 3;
47     // Scroll thumb fading out due to inactivity timeout
48     private static final int STATE_EXIT = 4;
49 
50     private Drawable mThumbDrawable;
51     private Drawable mOverlayDrawable;
52 
53     private int mThumbH;
54     private int mThumbW;
55     private int mThumbY;
56 
57     private RectF mOverlayPos;
58     private int mOverlaySize;
59 
60     private AbsListView mList;
61     private boolean mScrollCompleted;
62     private int mVisibleItem;
63     private Paint mPaint;
64     private int mListOffset;
65     private int mItemCount = -1;
66     private boolean mLongList;
67 
68     private Object [] mSections;
69     private String mSectionText;
70     private boolean mDrawOverlay;
71     private ScrollFade mScrollFade;
72 
73     private int mState;
74 
75     private Handler mHandler = new Handler();
76 
77     private BaseAdapter mListAdapter;
78     private SectionIndexer mSectionIndexer;
79 
80     private boolean mChangedBounds;
81 
FastScroller(Context context, AbsListView listView)82     public FastScroller(Context context, AbsListView listView) {
83         mList = listView;
84         init(context);
85     }
86 
setState(int state)87     public void setState(int state) {
88         switch (state) {
89             case STATE_NONE:
90                 mHandler.removeCallbacks(mScrollFade);
91                 mList.invalidate();
92                 break;
93             case STATE_VISIBLE:
94                 if (mState != STATE_VISIBLE) { // Optimization
95                     resetThumbPos();
96                 }
97                 // Fall through
98             case STATE_DRAGGING:
99                 mHandler.removeCallbacks(mScrollFade);
100                 break;
101             case STATE_EXIT:
102                 int viewWidth = mList.getWidth();
103                 mList.invalidate(viewWidth - mThumbW, mThumbY, viewWidth, mThumbY + mThumbH);
104                 break;
105         }
106         mState = state;
107     }
108 
getState()109     public int getState() {
110         return mState;
111     }
112 
resetThumbPos()113     private void resetThumbPos() {
114         final int viewWidth = mList.getWidth();
115         // Bounds are always top right. Y coordinate get's translated during draw
116         mThumbDrawable.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH);
117         mThumbDrawable.setAlpha(ScrollFade.ALPHA_MAX);
118     }
119 
useThumbDrawable(Context context, Drawable drawable)120     private void useThumbDrawable(Context context, Drawable drawable) {
121         mThumbDrawable = drawable;
122         mThumbW = context.getResources().getDimensionPixelSize(
123                 com.android.internal.R.dimen.fastscroll_thumb_width);
124         mThumbH = context.getResources().getDimensionPixelSize(
125                 com.android.internal.R.dimen.fastscroll_thumb_height);
126         mChangedBounds = true;
127     }
128 
init(Context context)129     private void init(Context context) {
130         // Get both the scrollbar states drawables
131         final Resources res = context.getResources();
132         useThumbDrawable(context, res.getDrawable(
133                 com.android.internal.R.drawable.scrollbar_handle_accelerated_anim2));
134 
135         mOverlayDrawable = res.getDrawable(
136                 com.android.internal.R.drawable.menu_submenu_background);
137 
138         mScrollCompleted = true;
139 
140         getSectionsFromIndexer();
141 
142         mOverlaySize = context.getResources().getDimensionPixelSize(
143                 com.android.internal.R.dimen.fastscroll_overlay_size);
144         mOverlayPos = new RectF();
145         mScrollFade = new ScrollFade();
146         mPaint = new Paint();
147         mPaint.setAntiAlias(true);
148         mPaint.setTextAlign(Paint.Align.CENTER);
149         mPaint.setTextSize(mOverlaySize / 2);
150         TypedArray ta = context.getTheme().obtainStyledAttributes(new int[] {
151                 android.R.attr.textColorPrimary });
152         ColorStateList textColor = ta.getColorStateList(ta.getIndex(0));
153         int textColorNormal = textColor.getDefaultColor();
154         mPaint.setColor(textColorNormal);
155         mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
156 
157         mState = STATE_NONE;
158     }
159 
stop()160     void stop() {
161         setState(STATE_NONE);
162     }
163 
isVisible()164     boolean isVisible() {
165         return !(mState == STATE_NONE);
166     }
167 
draw(Canvas canvas)168     public void draw(Canvas canvas) {
169 
170         if (mState == STATE_NONE) {
171             // No need to draw anything
172             return;
173         }
174 
175         final int y = mThumbY;
176         final int viewWidth = mList.getWidth();
177         final FastScroller.ScrollFade scrollFade = mScrollFade;
178 
179         int alpha = -1;
180         if (mState == STATE_EXIT) {
181             alpha = scrollFade.getAlpha();
182             if (alpha < ScrollFade.ALPHA_MAX / 2) {
183                 mThumbDrawable.setAlpha(alpha * 2);
184             }
185             int left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
186             mThumbDrawable.setBounds(left, 0, viewWidth, mThumbH);
187             mChangedBounds = true;
188         }
189 
190         canvas.translate(0, y);
191         mThumbDrawable.draw(canvas);
192         canvas.translate(0, -y);
193 
194         // If user is dragging the scroll bar, draw the alphabet overlay
195         if (mState == STATE_DRAGGING && mDrawOverlay) {
196             mOverlayDrawable.draw(canvas);
197             final Paint paint = mPaint;
198             float descent = paint.descent();
199             final RectF rectF = mOverlayPos;
200             canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2,
201                     (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent, paint);
202         } else if (mState == STATE_EXIT) {
203             if (alpha == 0) { // Done with exit
204                 setState(STATE_NONE);
205             } else {
206                 mList.invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);
207             }
208         }
209     }
210 
onSizeChanged(int w, int h, int oldw, int oldh)211     void onSizeChanged(int w, int h, int oldw, int oldh) {
212         if (mThumbDrawable != null) {
213             mThumbDrawable.setBounds(w - mThumbW, 0, w, mThumbH);
214         }
215         final RectF pos = mOverlayPos;
216         pos.left = (w - mOverlaySize) / 2;
217         pos.right = pos.left + mOverlaySize;
218         pos.top = h / 10; // 10% from top
219         pos.bottom = pos.top + mOverlaySize;
220         if (mOverlayDrawable != null) {
221             mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
222                 (int) pos.right, (int) pos.bottom);
223         }
224     }
225 
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)226     void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
227             int totalItemCount) {
228         // Are there enough pages to require fast scroll? Recompute only if total count changes
229         if (mItemCount != totalItemCount && visibleItemCount > 0) {
230             mItemCount = totalItemCount;
231             mLongList = mItemCount / visibleItemCount >= MIN_PAGES;
232         }
233         if (!mLongList) {
234             if (mState != STATE_NONE) {
235                 setState(STATE_NONE);
236             }
237             return;
238         }
239         if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING ) {
240             mThumbY = ((mList.getHeight() - mThumbH) * firstVisibleItem)
241                     / (totalItemCount - visibleItemCount);
242             if (mChangedBounds) {
243                 resetThumbPos();
244                 mChangedBounds = false;
245             }
246         }
247         mScrollCompleted = true;
248         if (firstVisibleItem == mVisibleItem) {
249             return;
250         }
251         mVisibleItem = firstVisibleItem;
252         if (mState != STATE_DRAGGING) {
253             setState(STATE_VISIBLE);
254             mHandler.postDelayed(mScrollFade, 1500);
255         }
256     }
257 
getSectionIndexer()258     SectionIndexer getSectionIndexer() {
259         return mSectionIndexer;
260     }
261 
getSections()262     Object[] getSections() {
263         if (mListAdapter == null && mList != null) {
264             getSectionsFromIndexer();
265         }
266         return mSections;
267     }
268 
getSectionsFromIndexer()269     private void getSectionsFromIndexer() {
270         Adapter adapter = mList.getAdapter();
271         mSectionIndexer = null;
272         if (adapter instanceof HeaderViewListAdapter) {
273             mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
274             adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
275         }
276         if (adapter instanceof ExpandableListConnector) {
277             ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter();
278             if (expAdapter instanceof SectionIndexer) {
279                 mSectionIndexer = (SectionIndexer) expAdapter;
280                 mListAdapter = (BaseAdapter) adapter;
281                 mSections = mSectionIndexer.getSections();
282             }
283         } else {
284             if (adapter instanceof SectionIndexer) {
285                 mListAdapter = (BaseAdapter) adapter;
286                 mSectionIndexer = (SectionIndexer) adapter;
287                 mSections = mSectionIndexer.getSections();
288 
289             } else {
290                 mListAdapter = (BaseAdapter) adapter;
291                 mSections = new String[] { " " };
292             }
293         }
294     }
295 
scrollTo(float position)296     private void scrollTo(float position) {
297         int count = mList.getCount();
298         mScrollCompleted = false;
299         float fThreshold = (1.0f / count) / 8;
300         final Object[] sections = mSections;
301         int sectionIndex;
302         if (sections != null && sections.length > 1) {
303             final int nSections = sections.length;
304             int section = (int) (position * nSections);
305             if (section >= nSections) {
306                 section = nSections - 1;
307             }
308             int exactSection = section;
309             sectionIndex = section;
310             int index = mSectionIndexer.getPositionForSection(section);
311             // Given the expected section and index, the following code will
312             // try to account for missing sections (no names starting with..)
313             // It will compute the scroll space of surrounding empty sections
314             // and interpolate the currently visible letter's range across the
315             // available space, so that there is always some list movement while
316             // the user moves the thumb.
317             int nextIndex = count;
318             int prevIndex = index;
319             int prevSection = section;
320             int nextSection = section + 1;
321             // Assume the next section is unique
322             if (section < nSections - 1) {
323                 nextIndex = mSectionIndexer.getPositionForSection(section + 1);
324             }
325 
326             // Find the previous index if we're slicing the previous section
327             if (nextIndex == index) {
328                 // Non-existent letter
329                 while (section > 0) {
330                     section--;
331                     prevIndex = mSectionIndexer.getPositionForSection(section);
332                     if (prevIndex != index) {
333                         prevSection = section;
334                         sectionIndex = section;
335                         break;
336                     } else if (section == 0) {
337                         // When section reaches 0 here, sectionIndex must follow it.
338                         // Assuming mSectionIndexer.getPositionForSection(0) == 0.
339                         sectionIndex = 0;
340                         break;
341                     }
342                 }
343             }
344             // Find the next index, in case the assumed next index is not
345             // unique. For instance, if there is no P, then request for P's
346             // position actually returns Q's. So we need to look ahead to make
347             // sure that there is really a Q at Q's position. If not, move
348             // further down...
349             int nextNextSection = nextSection + 1;
350             while (nextNextSection < nSections &&
351                     mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
352                 nextNextSection++;
353                 nextSection++;
354             }
355             // Compute the beginning and ending scroll range percentage of the
356             // currently visible letter. This could be equal to or greater than
357             // (1 / nSections).
358             float fPrev = (float) prevSection / nSections;
359             float fNext = (float) nextSection / nSections;
360             if (prevSection == exactSection && position - fPrev < fThreshold) {
361                 index = prevIndex;
362             } else {
363                 index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev)
364                     / (fNext - fPrev));
365             }
366             // Don't overflow
367             if (index > count - 1) index = count - 1;
368 
369             if (mList instanceof ExpandableListView) {
370                 ExpandableListView expList = (ExpandableListView) mList;
371                 expList.setSelectionFromTop(expList.getFlatListPosition(
372                         ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
373             } else if (mList instanceof ListView) {
374                 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
375             } else {
376                 mList.setSelection(index + mListOffset);
377             }
378         } else {
379             int index = (int) (position * count);
380             if (mList instanceof ExpandableListView) {
381                 ExpandableListView expList = (ExpandableListView) mList;
382                 expList.setSelectionFromTop(expList.getFlatListPosition(
383                         ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
384             } else if (mList instanceof ListView) {
385                 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
386             } else {
387                 mList.setSelection(index + mListOffset);
388             }
389             sectionIndex = -1;
390         }
391 
392         if (sectionIndex >= 0) {
393             String text = mSectionText = sections[sectionIndex].toString();
394             mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') &&
395                     sectionIndex < sections.length;
396         } else {
397             mDrawOverlay = false;
398         }
399     }
400 
401     private void cancelFling() {
402         // Cancel the list fling
403         MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
404         mList.onTouchEvent(cancelFling);
405         cancelFling.recycle();
406     }
407 
408     boolean onInterceptTouchEvent(MotionEvent ev) {
409         if (mState > STATE_NONE && ev.getAction() == MotionEvent.ACTION_DOWN) {
410             if (isPointInside(ev.getX(), ev.getY())) {
411                 setState(STATE_DRAGGING);
412                 return true;
413             }
414         }
415         return false;
416     }
417 
onTouchEvent(MotionEvent me)418     boolean onTouchEvent(MotionEvent me) {
419         if (mState == STATE_NONE) {
420             return false;
421         }
422 
423         final int action = me.getAction();
424 
425         if (action == MotionEvent.ACTION_DOWN) {
426             if (isPointInside(me.getX(), me.getY())) {
427                 setState(STATE_DRAGGING);
428                 if (mListAdapter == null && mList != null) {
429                     getSectionsFromIndexer();
430                 }
431 
432                 cancelFling();
433                 return true;
434             }
435         } else if (action == MotionEvent.ACTION_UP) {
436             if (mState == STATE_DRAGGING) {
437                 setState(STATE_VISIBLE);
438                 final Handler handler = mHandler;
439                 handler.removeCallbacks(mScrollFade);
440                 handler.postDelayed(mScrollFade, 1000);
441                 return true;
442             }
443         } else if (action == MotionEvent.ACTION_MOVE) {
444             if (mState == STATE_DRAGGING) {
445                 final int viewHeight = mList.getHeight();
446                 // Jitter
447                 int newThumbY = (int) me.getY() - mThumbH + 10;
448                 if (newThumbY < 0) {
449                     newThumbY = 0;
450                 } else if (newThumbY + mThumbH > viewHeight) {
451                     newThumbY = viewHeight - mThumbH;
452                 }
453                 if (Math.abs(mThumbY - newThumbY) < 2) {
454                     return true;
455                 }
456                 mThumbY = newThumbY;
457                 // If the previous scrollTo is still pending
458                 if (mScrollCompleted) {
459                     scrollTo((float) mThumbY / (viewHeight - mThumbH));
460                 }
461                 return true;
462             }
463         }
464         return false;
465     }
466 
isPointInside(float x, float y)467     boolean isPointInside(float x, float y) {
468         return x > mList.getWidth() - mThumbW && y >= mThumbY && y <= mThumbY + mThumbH;
469     }
470 
471     public class ScrollFade implements Runnable {
472 
473         long mStartTime;
474         long mFadeDuration;
475         static final int ALPHA_MAX = 208;
476         static final long FADE_DURATION = 200;
477 
startFade()478         void startFade() {
479             mFadeDuration = FADE_DURATION;
480             mStartTime = SystemClock.uptimeMillis();
481             setState(STATE_EXIT);
482         }
483 
getAlpha()484         int getAlpha() {
485             if (getState() != STATE_EXIT) {
486                 return ALPHA_MAX;
487             }
488             int alpha;
489             long now = SystemClock.uptimeMillis();
490             if (now > mStartTime + mFadeDuration) {
491                 alpha = 0;
492             } else {
493                 alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration);
494             }
495             return alpha;
496         }
497 
run()498         public void run() {
499             if (getState() != STATE_EXIT) {
500                 startFade();
501                 return;
502             }
503 
504             if (getAlpha() > 0) {
505                 mList.invalidate();
506             } else {
507                 setState(STATE_NONE);
508             }
509         }
510     }
511 }
512