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