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