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