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