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