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.TypedArray; 22 import android.graphics.Canvas; 23 import android.graphics.Paint; 24 import android.graphics.Rect; 25 import android.graphics.RectF; 26 import android.graphics.drawable.Drawable; 27 import android.graphics.drawable.NinePatchDrawable; 28 import android.os.Handler; 29 import android.os.SystemClock; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewConfiguration; 33 import android.widget.AbsListView.OnScrollListener; 34 35 /** 36 * Helper class for AbsListView to draw and control the Fast Scroll thumb 37 */ 38 class FastScroller { 39 private static final String TAG = "FastScroller"; 40 41 // Minimum number of pages to justify showing a fast scroll thumb 42 private static int MIN_PAGES = 4; 43 // Scroll thumb not showing 44 private static final int STATE_NONE = 0; 45 // Not implemented yet - fade-in transition 46 private static final int STATE_ENTER = 1; 47 // Scroll thumb visible and moving along with the scrollbar 48 private static final int STATE_VISIBLE = 2; 49 // Scroll thumb being dragged by user 50 private static final int STATE_DRAGGING = 3; 51 // Scroll thumb fading out due to inactivity timeout 52 private static final int STATE_EXIT = 4; 53 54 private static final int[] PRESSED_STATES = new int[] { 55 android.R.attr.state_pressed 56 }; 57 58 private static final int[] DEFAULT_STATES = new int[0]; 59 60 private static final int[] ATTRS = new int[] { 61 android.R.attr.fastScrollTextColor, 62 android.R.attr.fastScrollThumbDrawable, 63 android.R.attr.fastScrollTrackDrawable, 64 android.R.attr.fastScrollPreviewBackgroundLeft, 65 android.R.attr.fastScrollPreviewBackgroundRight, 66 android.R.attr.fastScrollOverlayPosition 67 }; 68 69 private static final int TEXT_COLOR = 0; 70 private static final int THUMB_DRAWABLE = 1; 71 private static final int TRACK_DRAWABLE = 2; 72 private static final int PREVIEW_BACKGROUND_LEFT = 3; 73 private static final int PREVIEW_BACKGROUND_RIGHT = 4; 74 private static final int OVERLAY_POSITION = 5; 75 76 private static final int OVERLAY_FLOATING = 0; 77 private static final int OVERLAY_AT_THUMB = 1; 78 79 private Drawable mThumbDrawable; 80 private Drawable mOverlayDrawable; 81 private Drawable mTrackDrawable; 82 83 private Drawable mOverlayDrawableLeft; 84 private Drawable mOverlayDrawableRight; 85 86 int mThumbH; 87 int mThumbW; 88 int mThumbY; 89 90 private RectF mOverlayPos; 91 private int mOverlaySize; 92 93 AbsListView mList; 94 boolean mScrollCompleted; 95 private int mVisibleItem; 96 private Paint mPaint; 97 private int mListOffset; 98 private int mItemCount = -1; 99 private boolean mLongList; 100 101 private Object [] mSections; 102 private String mSectionText; 103 private boolean mDrawOverlay; 104 private ScrollFade mScrollFade; 105 106 private int mState; 107 108 private Handler mHandler = new Handler(); 109 110 BaseAdapter mListAdapter; 111 private SectionIndexer mSectionIndexer; 112 113 private boolean mChangedBounds; 114 115 private int mPosition; 116 117 private boolean mAlwaysShow; 118 119 private int mOverlayPosition; 120 121 private boolean mMatchDragPosition; 122 123 float mInitialTouchY; 124 boolean mPendingDrag; 125 private int mScaledTouchSlop; 126 127 private static final int FADE_TIMEOUT = 1500; 128 private static final int PENDING_DRAG_DELAY = 180; 129 130 private final Rect mTmpRect = new Rect(); 131 132 private final Runnable mDeferStartDrag = new Runnable() { 133 public void run() { 134 if (mList.mIsAttached) { 135 beginDrag(); 136 137 final int viewHeight = mList.getHeight(); 138 // Jitter 139 int newThumbY = (int) mInitialTouchY - mThumbH + 10; 140 if (newThumbY < 0) { 141 newThumbY = 0; 142 } else if (newThumbY + mThumbH > viewHeight) { 143 newThumbY = viewHeight - mThumbH; 144 } 145 mThumbY = newThumbY; 146 scrollTo((float) mThumbY / (viewHeight - mThumbH)); 147 } 148 149 mPendingDrag = false; 150 } 151 }; 152 153 public FastScroller(Context context, AbsListView listView) { 154 mList = listView; 155 init(context); 156 } 157 158 public void setAlwaysShow(boolean alwaysShow) { 159 mAlwaysShow = alwaysShow; 160 if (alwaysShow) { 161 mHandler.removeCallbacks(mScrollFade); 162 setState(STATE_VISIBLE); 163 } else if (mState == STATE_VISIBLE) { 164 mHandler.postDelayed(mScrollFade, FADE_TIMEOUT); 165 } 166 } 167 168 public boolean isAlwaysShowEnabled() { 169 return mAlwaysShow; 170 } 171 172 private void refreshDrawableState() { 173 int[] state = mState == STATE_DRAGGING ? PRESSED_STATES : DEFAULT_STATES; 174 175 if (mThumbDrawable != null && mThumbDrawable.isStateful()) { 176 mThumbDrawable.setState(state); 177 } 178 if (mTrackDrawable != null && mTrackDrawable.isStateful()) { 179 mTrackDrawable.setState(state); 180 } 181 } 182 183 public void setScrollbarPosition(int position) { 184 if (position == View.SCROLLBAR_POSITION_DEFAULT) { 185 position = mList.isLayoutRtl() ? 186 View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT; 187 } 188 mPosition = position; 189 switch (position) { 190 default: 191 case View.SCROLLBAR_POSITION_RIGHT: 192 mOverlayDrawable = mOverlayDrawableRight; 193 break; 194 case View.SCROLLBAR_POSITION_LEFT: 195 mOverlayDrawable = mOverlayDrawableLeft; 196 break; 197 } 198 } 199 200 public int getWidth() { 201 return mThumbW; 202 } 203 204 public void setState(int state) { 205 switch (state) { 206 case STATE_NONE: 207 mHandler.removeCallbacks(mScrollFade); 208 mList.invalidate(); 209 break; 210 case STATE_VISIBLE: 211 if (mState != STATE_VISIBLE) { // Optimization 212 resetThumbPos(); 213 } 214 // Fall through 215 case STATE_DRAGGING: 216 mHandler.removeCallbacks(mScrollFade); 217 break; 218 case STATE_EXIT: 219 final int viewWidth = mList.getWidth(); 220 final int top = mThumbY; 221 final int bottom = mThumbY + mThumbH; 222 final int left; 223 final int right; 224 switch (mList.getLayoutDirection()) { 225 case View.LAYOUT_DIRECTION_RTL: 226 left = 0; 227 right = mThumbW; 228 break; 229 case View.LAYOUT_DIRECTION_LTR: 230 default: 231 left = viewWidth - mThumbW; 232 right = viewWidth; 233 } 234 mList.invalidate(left, top, right, bottom); 235 break; 236 } 237 mState = state; 238 refreshDrawableState(); 239 } 240 241 public int getState() { 242 return mState; 243 } 244 245 private void resetThumbPos() { 246 final int viewWidth = mList.getWidth(); 247 // Bounds are always top right. Y coordinate get's translated during draw 248 switch (mPosition) { 249 case View.SCROLLBAR_POSITION_RIGHT: 250 mThumbDrawable.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH); 251 break; 252 case View.SCROLLBAR_POSITION_LEFT: 253 mThumbDrawable.setBounds(0, 0, mThumbW, mThumbH); 254 break; 255 } 256 mThumbDrawable.setAlpha(ScrollFade.ALPHA_MAX); 257 } 258 259 private void useThumbDrawable(Context context, Drawable drawable) { 260 mThumbDrawable = drawable; 261 if (drawable instanceof NinePatchDrawable) { 262 mThumbW = context.getResources().getDimensionPixelSize( 263 com.android.internal.R.dimen.fastscroll_thumb_width); 264 mThumbH = context.getResources().getDimensionPixelSize( 265 com.android.internal.R.dimen.fastscroll_thumb_height); 266 } else { 267 mThumbW = drawable.getIntrinsicWidth(); 268 mThumbH = drawable.getIntrinsicHeight(); 269 } 270 mChangedBounds = true; 271 } 272 273 private void init(Context context) { 274 // Get both the scrollbar states drawables 275 TypedArray ta = context.getTheme().obtainStyledAttributes(ATTRS); 276 useThumbDrawable(context, ta.getDrawable(THUMB_DRAWABLE)); 277 mTrackDrawable = ta.getDrawable(TRACK_DRAWABLE); 278 279 mOverlayDrawableLeft = ta.getDrawable(PREVIEW_BACKGROUND_LEFT); 280 mOverlayDrawableRight = ta.getDrawable(PREVIEW_BACKGROUND_RIGHT); 281 mOverlayPosition = ta.getInt(OVERLAY_POSITION, OVERLAY_FLOATING); 282 283 mScrollCompleted = true; 284 285 getSectionsFromIndexer(); 286 287 mOverlaySize = context.getResources().getDimensionPixelSize( 288 com.android.internal.R.dimen.fastscroll_overlay_size); 289 mOverlayPos = new RectF(); 290 mScrollFade = new ScrollFade(); 291 mPaint = new Paint(); 292 mPaint.setAntiAlias(true); 293 mPaint.setTextAlign(Paint.Align.CENTER); 294 mPaint.setTextSize(mOverlaySize / 2); 295 296 ColorStateList textColor = ta.getColorStateList(TEXT_COLOR); 297 int textColorNormal = textColor.getDefaultColor(); 298 mPaint.setColor(textColorNormal); 299 mPaint.setStyle(Paint.Style.FILL_AND_STROKE); 300 301 // to show mOverlayDrawable properly 302 if (mList.getWidth() > 0 && mList.getHeight() > 0) { 303 onSizeChanged(mList.getWidth(), mList.getHeight(), 0, 0); 304 } 305 306 mState = STATE_NONE; 307 refreshDrawableState(); 308 309 ta.recycle(); 310 311 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 312 313 mMatchDragPosition = context.getApplicationInfo().targetSdkVersion >= 314 android.os.Build.VERSION_CODES.HONEYCOMB; 315 316 setScrollbarPosition(mList.getVerticalScrollbarPosition()); 317 } 318 319 void stop() { 320 setState(STATE_NONE); 321 } 322 323 boolean isVisible() { 324 return !(mState == STATE_NONE); 325 } 326 327 public void draw(Canvas canvas) { 328 329 if (mState == STATE_NONE) { 330 // No need to draw anything 331 return; 332 } 333 334 final int y = mThumbY; 335 final int viewWidth = mList.getWidth(); 336 final FastScroller.ScrollFade scrollFade = mScrollFade; 337 338 int alpha = -1; 339 if (mState == STATE_EXIT) { 340 alpha = scrollFade.getAlpha(); 341 if (alpha < ScrollFade.ALPHA_MAX / 2) { 342 mThumbDrawable.setAlpha(alpha * 2); 343 } 344 int left = 0; 345 switch (mPosition) { 346 case View.SCROLLBAR_POSITION_RIGHT: 347 left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX; 348 break; 349 case View.SCROLLBAR_POSITION_LEFT: 350 left = -mThumbW + (mThumbW * alpha) / ScrollFade.ALPHA_MAX; 351 break; 352 } 353 mThumbDrawable.setBounds(left, 0, left + mThumbW, mThumbH); 354 mChangedBounds = true; 355 } 356 357 if (mTrackDrawable != null) { 358 final Rect thumbBounds = mThumbDrawable.getBounds(); 359 final int left = thumbBounds.left; 360 final int halfThumbHeight = (thumbBounds.bottom - thumbBounds.top) / 2; 361 final int trackWidth = mTrackDrawable.getIntrinsicWidth(); 362 final int trackLeft = (left + mThumbW / 2) - trackWidth / 2; 363 mTrackDrawable.setBounds(trackLeft, halfThumbHeight, 364 trackLeft + trackWidth, mList.getHeight() - halfThumbHeight); 365 mTrackDrawable.draw(canvas); 366 } 367 368 canvas.translate(0, y); 369 mThumbDrawable.draw(canvas); 370 canvas.translate(0, -y); 371 372 // If user is dragging the scroll bar, draw the alphabet overlay 373 if (mState == STATE_DRAGGING && mDrawOverlay) { 374 if (mOverlayPosition == OVERLAY_AT_THUMB) { 375 int left = 0; 376 switch (mPosition) { 377 default: 378 case View.SCROLLBAR_POSITION_RIGHT: 379 left = Math.max(0, 380 mThumbDrawable.getBounds().left - mThumbW - mOverlaySize); 381 break; 382 case View.SCROLLBAR_POSITION_LEFT: 383 left = Math.min(mThumbDrawable.getBounds().right + mThumbW, 384 mList.getWidth() - mOverlaySize); 385 break; 386 } 387 388 int top = Math.max(0, 389 Math.min(y + (mThumbH - mOverlaySize) / 2, mList.getHeight() - mOverlaySize)); 390 391 final RectF pos = mOverlayPos; 392 pos.left = left; 393 pos.right = pos.left + mOverlaySize; 394 pos.top = top; 395 pos.bottom = pos.top + mOverlaySize; 396 if (mOverlayDrawable != null) { 397 mOverlayDrawable.setBounds((int) pos.left, (int) pos.top, 398 (int) pos.right, (int) pos.bottom); 399 } 400 } 401 mOverlayDrawable.draw(canvas); 402 final Paint paint = mPaint; 403 float descent = paint.descent(); 404 final RectF rectF = mOverlayPos; 405 final Rect tmpRect = mTmpRect; 406 mOverlayDrawable.getPadding(tmpRect); 407 final int hOff = (tmpRect.right - tmpRect.left) / 2; 408 final int vOff = (tmpRect.bottom - tmpRect.top) / 2; 409 canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2 - hOff, 410 (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent - vOff, 411 paint); 412 } else if (mState == STATE_EXIT) { 413 if (alpha == 0) { // Done with exit 414 setState(STATE_NONE); 415 } else { 416 final int left, right, top, bottom; 417 if (mTrackDrawable != null) { 418 top = 0; 419 bottom = mList.getHeight(); 420 } else { 421 top = y; 422 bottom = y + mThumbH; 423 } 424 switch (mList.getLayoutDirection()) { 425 case View.LAYOUT_DIRECTION_RTL: 426 left = 0; 427 right = mThumbW; 428 break; 429 case View.LAYOUT_DIRECTION_LTR: 430 default: 431 left = viewWidth - mThumbW; 432 right = viewWidth; 433 } 434 mList.invalidate(left, top, right, bottom); 435 } 436 } 437 } 438 439 void onSizeChanged(int w, int h, int oldw, int oldh) { 440 if (mThumbDrawable != null) { 441 switch (mPosition) { 442 default: 443 case View.SCROLLBAR_POSITION_RIGHT: 444 mThumbDrawable.setBounds(w - mThumbW, 0, w, mThumbH); 445 break; 446 case View.SCROLLBAR_POSITION_LEFT: 447 mThumbDrawable.setBounds(0, 0, mThumbW, mThumbH); 448 break; 449 } 450 } 451 if (mOverlayPosition == OVERLAY_FLOATING) { 452 final RectF pos = mOverlayPos; 453 pos.left = (w - mOverlaySize) / 2; 454 pos.right = pos.left + mOverlaySize; 455 pos.top = h / 10; // 10% from top 456 pos.bottom = pos.top + mOverlaySize; 457 if (mOverlayDrawable != null) { 458 mOverlayDrawable.setBounds((int) pos.left, (int) pos.top, 459 (int) pos.right, (int) pos.bottom); 460 } 461 } 462 } 463 464 void onItemCountChanged(int oldCount, int newCount) { 465 if (mAlwaysShow) { 466 mLongList = true; 467 } 468 } 469 470 void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 471 int totalItemCount) { 472 // Are there enough pages to require fast scroll? Recompute only if total count changes 473 if (mItemCount != totalItemCount && visibleItemCount > 0) { 474 mItemCount = totalItemCount; 475 mLongList = mItemCount / visibleItemCount >= MIN_PAGES; 476 } 477 if (mAlwaysShow) { 478 mLongList = true; 479 } 480 if (!mLongList) { 481 if (mState != STATE_NONE) { 482 setState(STATE_NONE); 483 } 484 return; 485 } 486 if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING) { 487 mThumbY = getThumbPositionForListPosition(firstVisibleItem, visibleItemCount, 488 totalItemCount); 489 if (mChangedBounds) { 490 resetThumbPos(); 491 mChangedBounds = false; 492 } 493 } 494 mScrollCompleted = true; 495 if (firstVisibleItem == mVisibleItem) { 496 return; 497 } 498 mVisibleItem = firstVisibleItem; 499 if (mState != STATE_DRAGGING) { 500 setState(STATE_VISIBLE); 501 if (!mAlwaysShow) { 502 mHandler.postDelayed(mScrollFade, FADE_TIMEOUT); 503 } 504 } 505 } 506 507 SectionIndexer getSectionIndexer() { 508 return mSectionIndexer; 509 } 510 511 Object[] getSections() { 512 if (mListAdapter == null && mList != null) { 513 getSectionsFromIndexer(); 514 } 515 return mSections; 516 } 517 518 void getSectionsFromIndexer() { 519 Adapter adapter = mList.getAdapter(); 520 mSectionIndexer = null; 521 if (adapter instanceof HeaderViewListAdapter) { 522 mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount(); 523 adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter(); 524 } 525 if (adapter instanceof ExpandableListConnector) { 526 ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter(); 527 if (expAdapter instanceof SectionIndexer) { 528 mSectionIndexer = (SectionIndexer) expAdapter; 529 mListAdapter = (BaseAdapter) adapter; 530 mSections = mSectionIndexer.getSections(); 531 } 532 } else { 533 if (adapter instanceof SectionIndexer) { 534 mListAdapter = (BaseAdapter) adapter; 535 mSectionIndexer = (SectionIndexer) adapter; 536 mSections = mSectionIndexer.getSections(); 537 if (mSections == null) { 538 mSections = new String[] { " " }; 539 } 540 } else { 541 mListAdapter = (BaseAdapter) adapter; 542 mSections = new String[] { " " }; 543 } 544 } 545 } 546 547 public void onSectionsChanged() { 548 mListAdapter = null; 549 } 550 551 void scrollTo(float position) { 552 int count = mList.getCount(); 553 mScrollCompleted = false; 554 float fThreshold = (1.0f / count) / 8; 555 final Object[] sections = mSections; 556 int sectionIndex; 557 if (sections != null && sections.length > 1) { 558 final int nSections = sections.length; 559 int section = (int) (position * nSections); 560 if (section >= nSections) { 561 section = nSections - 1; 562 } 563 int exactSection = section; 564 sectionIndex = section; 565 int index = mSectionIndexer.getPositionForSection(section); 566 // Given the expected section and index, the following code will 567 // try to account for missing sections (no names starting with..) 568 // It will compute the scroll space of surrounding empty sections 569 // and interpolate the currently visible letter's range across the 570 // available space, so that there is always some list movement while 571 // the user moves the thumb. 572 int nextIndex = count; 573 int prevIndex = index; 574 int prevSection = section; 575 int nextSection = section + 1; 576 // Assume the next section is unique 577 if (section < nSections - 1) { 578 nextIndex = mSectionIndexer.getPositionForSection(section + 1); 579 } 580 581 // Find the previous index if we're slicing the previous section 582 if (nextIndex == index) { 583 // Non-existent letter 584 while (section > 0) { 585 section--; 586 prevIndex = mSectionIndexer.getPositionForSection(section); 587 if (prevIndex != index) { 588 prevSection = section; 589 sectionIndex = section; 590 break; 591 } else if (section == 0) { 592 // When section reaches 0 here, sectionIndex must follow it. 593 // Assuming mSectionIndexer.getPositionForSection(0) == 0. 594 sectionIndex = 0; 595 break; 596 } 597 } 598 } 599 // Find the next index, in case the assumed next index is not 600 // unique. For instance, if there is no P, then request for P's 601 // position actually returns Q's. So we need to look ahead to make 602 // sure that there is really a Q at Q's position. If not, move 603 // further down... 604 int nextNextSection = nextSection + 1; 605 while (nextNextSection < nSections && 606 mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) { 607 nextNextSection++; 608 nextSection++; 609 } 610 // Compute the beginning and ending scroll range percentage of the 611 // currently visible letter. This could be equal to or greater than 612 // (1 / nSections). 613 float fPrev = (float) prevSection / nSections; 614 float fNext = (float) nextSection / nSections; 615 if (prevSection == exactSection && position - fPrev < fThreshold) { 616 index = prevIndex; 617 } else { 618 index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev) 619 / (fNext - fPrev)); 620 } 621 // Don't overflow 622 if (index > count - 1) index = count - 1; 623 624 if (mList instanceof ExpandableListView) { 625 ExpandableListView expList = (ExpandableListView) mList; 626 expList.setSelectionFromTop(expList.getFlatListPosition( 627 ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0); 628 } else if (mList instanceof ListView) { 629 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0); 630 } else { 631 mList.setSelection(index + mListOffset); 632 } 633 } else { 634 int index = (int) (position * count); 635 // Don't overflow 636 if (index > count - 1) index = count - 1; 637 638 if (mList instanceof ExpandableListView) { 639 ExpandableListView expList = (ExpandableListView) mList; 640 expList.setSelectionFromTop(expList.getFlatListPosition( 641 ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0); 642 } else if (mList instanceof ListView) { 643 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0); 644 } else { 645 mList.setSelection(index + mListOffset); 646 } 647 sectionIndex = -1; 648 } 649 650 if (sectionIndex >= 0) { 651 String text = mSectionText = sections[sectionIndex].toString(); 652 mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') && 653 sectionIndex < sections.length; 654 } else { 655 mDrawOverlay = false; 656 } 657 } 658 659 private int getThumbPositionForListPosition(int firstVisibleItem, int visibleItemCount, 660 int totalItemCount) { 661 if (mSectionIndexer == null || mListAdapter == null) { 662 getSectionsFromIndexer(); 663 } 664 if (mSectionIndexer == null || !mMatchDragPosition) { 665 return ((mList.getHeight() - mThumbH) * firstVisibleItem) 666 / (totalItemCount - visibleItemCount); 667 } 668 669 firstVisibleItem -= mListOffset; 670 if (firstVisibleItem < 0) { 671 return 0; 672 } 673 totalItemCount -= mListOffset; 674 675 final int trackHeight = mList.getHeight() - mThumbH; 676 677 final int section = mSectionIndexer.getSectionForPosition(firstVisibleItem); 678 final int sectionPos = mSectionIndexer.getPositionForSection(section); 679 final int nextSectionPos = mSectionIndexer.getPositionForSection(section + 1); 680 final int sectionCount = mSections.length; 681 final int positionsInSection = nextSectionPos - sectionPos; 682 683 final View child = mList.getChildAt(0); 684 final float incrementalPos = child == null ? 0 : firstVisibleItem + 685 (float) (mList.getPaddingTop() - child.getTop()) / child.getHeight(); 686 final float posWithinSection = (incrementalPos - sectionPos) / positionsInSection; 687 int result = (int) ((section + posWithinSection) / sectionCount * trackHeight); 688 689 // Fake out the scrollbar for the last item. Since the section indexer won't 690 // ever actually move the list in this end space, make scrolling across the last item 691 // account for whatever space is remaining. 692 if (firstVisibleItem > 0 && firstVisibleItem + visibleItemCount == totalItemCount) { 693 final View lastChild = mList.getChildAt(visibleItemCount - 1); 694 final float lastItemVisible = (float) (mList.getHeight() - mList.getPaddingBottom() 695 - lastChild.getTop()) / lastChild.getHeight(); 696 result += (trackHeight - result) * lastItemVisible; 697 } 698 699 return result; 700 } 701 702 private void cancelFling() { 703 // Cancel the list fling 704 MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0); 705 mList.onTouchEvent(cancelFling); 706 cancelFling.recycle(); 707 } 708 709 void cancelPendingDrag() { 710 mList.removeCallbacks(mDeferStartDrag); 711 mPendingDrag = false; 712 } 713 714 void startPendingDrag() { 715 mPendingDrag = true; 716 mList.postDelayed(mDeferStartDrag, PENDING_DRAG_DELAY); 717 } 718 719 void beginDrag() { 720 setState(STATE_DRAGGING); 721 if (mListAdapter == null && mList != null) { 722 getSectionsFromIndexer(); 723 } 724 if (mList != null) { 725 mList.requestDisallowInterceptTouchEvent(true); 726 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 727 } 728 729 cancelFling(); 730 } 731 732 boolean onInterceptTouchEvent(MotionEvent ev) { 733 switch (ev.getActionMasked()) { 734 case MotionEvent.ACTION_DOWN: 735 if (mState > STATE_NONE && isPointInside(ev.getX(), ev.getY())) { 736 if (!mList.isInScrollingContainer()) { 737 beginDrag(); 738 return true; 739 } 740 mInitialTouchY = ev.getY(); 741 startPendingDrag(); 742 } 743 break; 744 case MotionEvent.ACTION_UP: 745 case MotionEvent.ACTION_CANCEL: 746 cancelPendingDrag(); 747 break; 748 } 749 return false; 750 } 751 752 boolean onTouchEvent(MotionEvent me) { 753 if (mState == STATE_NONE) { 754 return false; 755 } 756 757 final int action = me.getAction(); 758 759 if (action == MotionEvent.ACTION_DOWN) { 760 if (isPointInside(me.getX(), me.getY())) { 761 if (!mList.isInScrollingContainer()) { 762 beginDrag(); 763 return true; 764 } 765 mInitialTouchY = me.getY(); 766 startPendingDrag(); 767 } 768 } else if (action == MotionEvent.ACTION_UP) { // don't add ACTION_CANCEL here 769 if (mPendingDrag) { 770 // Allow a tap to scroll. 771 beginDrag(); 772 773 final int viewHeight = mList.getHeight(); 774 // Jitter 775 int newThumbY = (int) me.getY() - mThumbH + 10; 776 if (newThumbY < 0) { 777 newThumbY = 0; 778 } else if (newThumbY + mThumbH > viewHeight) { 779 newThumbY = viewHeight - mThumbH; 780 } 781 mThumbY = newThumbY; 782 scrollTo((float) mThumbY / (viewHeight - mThumbH)); 783 784 cancelPendingDrag(); 785 // Will hit the STATE_DRAGGING check below 786 } 787 if (mState == STATE_DRAGGING) { 788 if (mList != null) { 789 // ViewGroup does the right thing already, but there might 790 // be other classes that don't properly reset on touch-up, 791 // so do this explicitly just in case. 792 mList.requestDisallowInterceptTouchEvent(false); 793 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 794 } 795 setState(STATE_VISIBLE); 796 final Handler handler = mHandler; 797 handler.removeCallbacks(mScrollFade); 798 if (!mAlwaysShow) { 799 handler.postDelayed(mScrollFade, 1000); 800 } 801 802 mList.invalidate(); 803 return true; 804 } 805 } else if (action == MotionEvent.ACTION_MOVE) { 806 if (mPendingDrag) { 807 final float y = me.getY(); 808 if (Math.abs(y - mInitialTouchY) > mScaledTouchSlop) { 809 setState(STATE_DRAGGING); 810 if (mListAdapter == null && mList != null) { 811 getSectionsFromIndexer(); 812 } 813 if (mList != null) { 814 mList.requestDisallowInterceptTouchEvent(true); 815 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 816 } 817 818 cancelFling(); 819 cancelPendingDrag(); 820 // Will hit the STATE_DRAGGING check below 821 } 822 } 823 if (mState == STATE_DRAGGING) { 824 final int viewHeight = mList.getHeight(); 825 // Jitter 826 int newThumbY = (int) me.getY() - mThumbH + 10; 827 if (newThumbY < 0) { 828 newThumbY = 0; 829 } else if (newThumbY + mThumbH > viewHeight) { 830 newThumbY = viewHeight - mThumbH; 831 } 832 if (Math.abs(mThumbY - newThumbY) < 2) { 833 return true; 834 } 835 mThumbY = newThumbY; 836 // If the previous scrollTo is still pending 837 if (mScrollCompleted) { 838 scrollTo((float) mThumbY / (viewHeight - mThumbH)); 839 } 840 return true; 841 } 842 } else if (action == MotionEvent.ACTION_CANCEL) { 843 cancelPendingDrag(); 844 } 845 return false; 846 } 847 848 boolean isPointInside(float x, float y) { 849 boolean inTrack = false; 850 switch (mPosition) { 851 default: 852 case View.SCROLLBAR_POSITION_RIGHT: 853 inTrack = x > mList.getWidth() - mThumbW; 854 break; 855 case View.SCROLLBAR_POSITION_LEFT: 856 inTrack = x < mThumbW; 857 break; 858 } 859 860 // Allow taps in the track to start moving. 861 return inTrack && (mTrackDrawable != null || y >= mThumbY && y <= mThumbY + mThumbH); 862 } 863 864 public class ScrollFade implements Runnable { 865 866 long mStartTime; 867 long mFadeDuration; 868 static final int ALPHA_MAX = 208; 869 static final long FADE_DURATION = 200; 870 871 void startFade() { 872 mFadeDuration = FADE_DURATION; 873 mStartTime = SystemClock.uptimeMillis(); 874 setState(STATE_EXIT); 875 } 876 877 int getAlpha() { 878 if (getState() != STATE_EXIT) { 879 return ALPHA_MAX; 880 } 881 int alpha; 882 long now = SystemClock.uptimeMillis(); 883 if (now > mStartTime + mFadeDuration) { 884 alpha = 0; 885 } else { 886 alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration); 887 } 888 return alpha; 889 } 890 891 public void run() { 892 if (getState() != STATE_EXIT) { 893 startFade(); 894 return; 895 } 896 897 if (getAlpha() > 0) { 898 mList.invalidate(); 899 } else { 900 setState(STATE_NONE); 901 } 902 } 903 } 904 } 905