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