FastScroller.java revision 1ffaebc41bfd47472543d1ff86c8412c868fc4c9
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;
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 = 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
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
160    void stop() {
161        setState(STATE_NONE);
162    }
163
164    boolean isVisible() {
165        return !(mState == STATE_NONE);
166    }
167
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
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
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
258    SectionIndexer getSectionIndexer() {
259        return mSectionIndexer;
260    }
261
262    Object[] getSections() {
263        if (mListAdapter == null && mList != null) {
264            getSectionsFromIndexer();
265        }
266        return mSections;
267    }
268
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
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
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
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
478        void startFade() {
479            mFadeDuration = FADE_DURATION;
480            mStartTime = SystemClock.uptimeMillis();
481            setState(STATE_EXIT);
482        }
483
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
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