FastScroller.java revision a5e441525660765e0913140daff4d12a0d94e88c
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.RectF;
26import android.graphics.drawable.Drawable;
27import android.os.Handler;
28import android.os.SystemClock;
29import android.util.TypedValue;
30import android.view.MotionEvent;
31
32/**
33 * Helper class for AbsListView to draw and control the Fast Scroll thumb
34 */
35class 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 = 104;
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
82    public FastScroller(Context context, AbsListView listView) {
83        mList = listView;
84        init(context);
85    }
86
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
109    public int getState() {
110        return mState;
111    }
112
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
120    private void useThumbDrawable(Context context, Drawable drawable) {
121        mThumbDrawable = drawable;
122        mThumbW = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
123                64, context.getResources().getDisplayMetrics());
124        mThumbH = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
125                52, context.getResources().getDisplayMetrics());
126        mChangedBounds = true;
127    }
128
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        mOverlayPos = new RectF();
143        mScrollFade = new ScrollFade();
144        mPaint = new Paint();
145        mPaint.setAntiAlias(true);
146        mPaint.setTextAlign(Paint.Align.CENTER);
147        mPaint.setTextSize(mOverlaySize / 2);
148        TypedArray ta = context.getTheme().obtainStyledAttributes(new int[] {
149                android.R.attr.textColorPrimary });
150        ColorStateList textColor = ta.getColorStateList(ta.getIndex(0));
151        int textColorNormal = textColor.getDefaultColor();
152        mPaint.setColor(textColorNormal);
153        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
154
155        mState = STATE_NONE;
156    }
157
158    void stop() {
159        setState(STATE_NONE);
160    }
161
162    boolean isVisible() {
163        return !(mState == STATE_NONE);
164    }
165
166    public void draw(Canvas canvas) {
167
168        if (mState == STATE_NONE) {
169            // No need to draw anything
170            return;
171        }
172
173        final int y = mThumbY;
174        final int viewWidth = mList.getWidth();
175        final FastScroller.ScrollFade scrollFade = mScrollFade;
176
177        int alpha = -1;
178        if (mState == STATE_EXIT) {
179            alpha = scrollFade.getAlpha();
180            if (alpha < ScrollFade.ALPHA_MAX / 2) {
181                mThumbDrawable.setAlpha(alpha * 2);
182            }
183            int left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
184            mThumbDrawable.setBounds(left, 0, viewWidth, mThumbH);
185            mChangedBounds = true;
186        }
187
188        canvas.translate(0, y);
189        mThumbDrawable.draw(canvas);
190        canvas.translate(0, -y);
191
192        // If user is dragging the scroll bar, draw the alphabet overlay
193        if (mState == STATE_DRAGGING && mDrawOverlay) {
194            mOverlayDrawable.draw(canvas);
195            final Paint paint = mPaint;
196            float descent = paint.descent();
197            final RectF rectF = mOverlayPos;
198            canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2,
199                    (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent, paint);
200        } else if (mState == STATE_EXIT) {
201            if (alpha == 0) { // Done with exit
202                setState(STATE_NONE);
203            } else {
204                mList.invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);
205            }
206        }
207    }
208
209    void onSizeChanged(int w, int h, int oldw, int oldh) {
210        if (mThumbDrawable != null) {
211            mThumbDrawable.setBounds(w - mThumbW, 0, w, mThumbH);
212        }
213        final RectF pos = mOverlayPos;
214        pos.left = (w - mOverlaySize) / 2;
215        pos.right = pos.left + mOverlaySize;
216        pos.top = h / 10; // 10% from top
217        pos.bottom = pos.top + mOverlaySize;
218        if (mOverlayDrawable != null) {
219            mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
220                (int) pos.right, (int) pos.bottom);
221        }
222    }
223
224    void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
225            int totalItemCount) {
226        // Are there enough pages to require fast scroll? Recompute only if total count changes
227        if (mItemCount != totalItemCount && visibleItemCount > 0) {
228            mItemCount = totalItemCount;
229            mLongList = mItemCount / visibleItemCount >= MIN_PAGES;
230        }
231        if (!mLongList) {
232            if (mState != STATE_NONE) {
233                setState(STATE_NONE);
234            }
235            return;
236        }
237        if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING ) {
238            mThumbY = ((mList.getHeight() - mThumbH) * firstVisibleItem)
239                    / (totalItemCount - visibleItemCount);
240            if (mChangedBounds) {
241                resetThumbPos();
242                mChangedBounds = false;
243            }
244        }
245        mScrollCompleted = true;
246        if (firstVisibleItem == mVisibleItem) {
247            return;
248        }
249        mVisibleItem = firstVisibleItem;
250        if (mState != STATE_DRAGGING) {
251            setState(STATE_VISIBLE);
252            mHandler.postDelayed(mScrollFade, 1500);
253        }
254    }
255
256    SectionIndexer getSectionIndexer() {
257        return mSectionIndexer;
258    }
259
260    Object[] getSections() {
261        if (mListAdapter == null && mList != null) {
262            getSectionsFromIndexer();
263        }
264        return mSections;
265    }
266
267    private void getSectionsFromIndexer() {
268        Adapter adapter = mList.getAdapter();
269        mSectionIndexer = null;
270        if (adapter instanceof HeaderViewListAdapter) {
271            mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
272            adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
273        }
274        if (adapter instanceof ExpandableListConnector) {
275            ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter();
276            if (expAdapter instanceof SectionIndexer) {
277                mSectionIndexer = (SectionIndexer) expAdapter;
278                mListAdapter = (BaseAdapter) adapter;
279                mSections = mSectionIndexer.getSections();
280            }
281        } else {
282            if (adapter instanceof SectionIndexer) {
283                mListAdapter = (BaseAdapter) adapter;
284                mSectionIndexer = (SectionIndexer) adapter;
285                mSections = mSectionIndexer.getSections();
286
287            } else {
288                mListAdapter = (BaseAdapter) adapter;
289                mSections = new String[] { " " };
290            }
291        }
292    }
293
294    private void scrollTo(float position) {
295        int count = mList.getCount();
296        mScrollCompleted = false;
297        float fThreshold = (1.0f / count) / 8;
298        final Object[] sections = mSections;
299        int sectionIndex;
300        if (sections != null && sections.length > 1) {
301            final int nSections = sections.length;
302            int section = (int) (position * nSections);
303            if (section >= nSections) {
304                section = nSections - 1;
305            }
306            int exactSection = section;
307            sectionIndex = section;
308            int index = mSectionIndexer.getPositionForSection(section);
309            // Given the expected section and index, the following code will
310            // try to account for missing sections (no names starting with..)
311            // It will compute the scroll space of surrounding empty sections
312            // and interpolate the currently visible letter's range across the
313            // available space, so that there is always some list movement while
314            // the user moves the thumb.
315            int nextIndex = count;
316            int prevIndex = index;
317            int prevSection = section;
318            int nextSection = section + 1;
319            // Assume the next section is unique
320            if (section < nSections - 1) {
321                nextIndex = mSectionIndexer.getPositionForSection(section + 1);
322            }
323
324            // Find the previous index if we're slicing the previous section
325            if (nextIndex == index) {
326                // Non-existent letter
327                while (section > 0) {
328                    section--;
329                    prevIndex = mSectionIndexer.getPositionForSection(section);
330                    if (prevIndex != index) {
331                        prevSection = section;
332                        sectionIndex = section;
333                        break;
334                    } else if (section == 0) {
335                        // When section reaches 0 here, sectionIndex must follow it.
336                        // Assuming mSectionIndexer.getPositionForSection(0) == 0.
337                        sectionIndex = 0;
338                        break;
339                    }
340                }
341            }
342            // Find the next index, in case the assumed next index is not
343            // unique. For instance, if there is no P, then request for P's
344            // position actually returns Q's. So we need to look ahead to make
345            // sure that there is really a Q at Q's position. If not, move
346            // further down...
347            int nextNextSection = nextSection + 1;
348            while (nextNextSection < nSections &&
349                    mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
350                nextNextSection++;
351                nextSection++;
352            }
353            // Compute the beginning and ending scroll range percentage of the
354            // currently visible letter. This could be equal to or greater than
355            // (1 / nSections).
356            float fPrev = (float) prevSection / nSections;
357            float fNext = (float) nextSection / nSections;
358            if (prevSection == exactSection && position - fPrev < fThreshold) {
359                index = prevIndex;
360            } else {
361                index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev)
362                    / (fNext - fPrev));
363            }
364            // Don't overflow
365            if (index > count - 1) index = count - 1;
366
367            if (mList instanceof ExpandableListView) {
368                ExpandableListView expList = (ExpandableListView) mList;
369                expList.setSelectionFromTop(expList.getFlatListPosition(
370                        ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
371            } else if (mList instanceof ListView) {
372                ((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
373            } else {
374                mList.setSelection(index + mListOffset);
375            }
376        } else {
377            int index = (int) (position * count);
378            if (mList instanceof ExpandableListView) {
379                ExpandableListView expList = (ExpandableListView) mList;
380                expList.setSelectionFromTop(expList.getFlatListPosition(
381                        ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
382            } else if (mList instanceof ListView) {
383                ((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
384            } else {
385                mList.setSelection(index + mListOffset);
386            }
387            sectionIndex = -1;
388        }
389
390        if (sectionIndex >= 0) {
391            String text = mSectionText = sections[sectionIndex].toString();
392            mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') &&
393                    sectionIndex < sections.length;
394        } else {
395            mDrawOverlay = false;
396        }
397    }
398
399    private void cancelFling() {
400        // Cancel the list fling
401        MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
402        mList.onTouchEvent(cancelFling);
403        cancelFling.recycle();
404    }
405
406    boolean onInterceptTouchEvent(MotionEvent ev) {
407        if (mState > STATE_NONE && ev.getAction() == MotionEvent.ACTION_DOWN) {
408            if (isPointInside(ev.getX(), ev.getY())) {
409                setState(STATE_DRAGGING);
410                return true;
411            }
412        }
413        return false;
414    }
415
416    boolean onTouchEvent(MotionEvent me) {
417        if (mState == STATE_NONE) {
418            return false;
419        }
420
421        final int action = me.getAction();
422
423        if (action == MotionEvent.ACTION_DOWN) {
424            if (isPointInside(me.getX(), me.getY())) {
425                setState(STATE_DRAGGING);
426                if (mListAdapter == null && mList != null) {
427                    getSectionsFromIndexer();
428                }
429
430                cancelFling();
431                return true;
432            }
433        } else if (action == MotionEvent.ACTION_UP) {
434            if (mState == STATE_DRAGGING) {
435                setState(STATE_VISIBLE);
436                final Handler handler = mHandler;
437                handler.removeCallbacks(mScrollFade);
438                handler.postDelayed(mScrollFade, 1000);
439                return true;
440            }
441        } else if (action == MotionEvent.ACTION_MOVE) {
442            if (mState == STATE_DRAGGING) {
443                final int viewHeight = mList.getHeight();
444                // Jitter
445                int newThumbY = (int) me.getY() - mThumbH + 10;
446                if (newThumbY < 0) {
447                    newThumbY = 0;
448                } else if (newThumbY + mThumbH > viewHeight) {
449                    newThumbY = viewHeight - mThumbH;
450                }
451                if (Math.abs(mThumbY - newThumbY) < 2) {
452                    return true;
453                }
454                mThumbY = newThumbY;
455                // If the previous scrollTo is still pending
456                if (mScrollCompleted) {
457                    scrollTo((float) mThumbY / (viewHeight - mThumbH));
458                }
459                return true;
460            }
461        }
462        return false;
463    }
464
465    boolean isPointInside(float x, float y) {
466        return x > mList.getWidth() - mThumbW && y >= mThumbY && y <= mThumbY + mThumbH;
467    }
468
469    public class ScrollFade implements Runnable {
470
471        long mStartTime;
472        long mFadeDuration;
473        static final int ALPHA_MAX = 208;
474        static final long FADE_DURATION = 200;
475
476        void startFade() {
477            mFadeDuration = FADE_DURATION;
478            mStartTime = SystemClock.uptimeMillis();
479            setState(STATE_EXIT);
480        }
481
482        int getAlpha() {
483            if (getState() != STATE_EXIT) {
484                return ALPHA_MAX;
485            }
486            int alpha;
487            long now = SystemClock.uptimeMillis();
488            if (now > mStartTime + mFadeDuration) {
489                alpha = 0;
490            } else {
491                alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration);
492            }
493            return alpha;
494        }
495
496        public void run() {
497            if (getState() != STATE_EXIT) {
498                startFade();
499                return;
500            }
501
502            if (getAlpha() > 0) {
503                mList.invalidate();
504            } else {
505                setState(STATE_NONE);
506            }
507        }
508    }
509}
510