Home | History | Annotate | Download | only in widget
      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 import android.widget.AbsListView.OnScrollListener;
     32 
     33 /**
     34  * Helper class for AbsListView to draw and control the Fast Scroll thumb
     35  */
     36 class 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