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