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