FastScroller.java revision 20232d0f7ce2220df72dd78bed052f6b4a643f10
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 com.android.internal.R.attr.fastScrollThumbDrawable, 62 com.android.internal.R.attr.fastScrollTrackDrawable, 63 com.android.internal.R.attr.fastScrollPreviewBackgroundLeft, 64 com.android.internal.R.attr.fastScrollPreviewBackgroundRight, 65 com.android.internal.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 final Resources res = context.getResources(); 231 TypedArray ta = context.getTheme().obtainStyledAttributes(ATTRS); 232 useThumbDrawable(context, ta.getDrawable(ta.getIndex(THUMB_DRAWABLE))); 233 mTrackDrawable = ta.getDrawable(ta.getIndex(TRACK_DRAWABLE)); 234 235 mOverlayDrawableLeft = ta.getDrawable(ta.getIndex(PREVIEW_BACKGROUND_LEFT)); 236 mOverlayDrawableRight = ta.getDrawable(ta.getIndex(PREVIEW_BACKGROUND_RIGHT)); 237 mOverlayPosition = ta.getInt(ta.getIndex(OVERLAY_POSITION), OVERLAY_FLOATING); 238 239 mScrollCompleted = true; 240 241 getSectionsFromIndexer(); 242 243 mOverlaySize = context.getResources().getDimensionPixelSize( 244 com.android.internal.R.dimen.fastscroll_overlay_size); 245 mOverlayPos = new RectF(); 246 mScrollFade = new ScrollFade(); 247 mPaint = new Paint(); 248 mPaint.setAntiAlias(true); 249 mPaint.setTextAlign(Paint.Align.CENTER); 250 mPaint.setTextSize(mOverlaySize / 2); 251 252 ColorStateList textColor = ta.getColorStateList(ta.getIndex(PRIMARY_TEXT_COLOR)); 253 int textColorNormal = textColor.getDefaultColor(); 254 mPaint.setColor(textColorNormal); 255 mPaint.setStyle(Paint.Style.FILL_AND_STROKE); 256 257 // to show mOverlayDrawable properly 258 if (mList.getWidth() > 0 && mList.getHeight() > 0) { 259 onSizeChanged(mList.getWidth(), mList.getHeight(), 0, 0); 260 } 261 262 mState = STATE_NONE; 263 refreshDrawableState(); 264 265 ta.recycle(); 266 267 setScrollbarPosition(mList.getVerticalScrollbarPosition()); 268 } 269 270 void stop() { 271 setState(STATE_NONE); 272 } 273 274 boolean isVisible() { 275 return !(mState == STATE_NONE); 276 } 277 278 public void draw(Canvas canvas) { 279 280 if (mState == STATE_NONE) { 281 // No need to draw anything 282 return; 283 } 284 285 final int y = mThumbY; 286 final int viewWidth = mList.getWidth(); 287 final FastScroller.ScrollFade scrollFade = mScrollFade; 288 289 int alpha = -1; 290 if (mState == STATE_EXIT) { 291 alpha = scrollFade.getAlpha(); 292 if (alpha < ScrollFade.ALPHA_MAX / 2) { 293 mThumbDrawable.setAlpha(alpha * 2); 294 } 295 int left = 0; 296 switch (mPosition) { 297 case View.SCROLLBAR_POSITION_DEFAULT: 298 case View.SCROLLBAR_POSITION_RIGHT: 299 left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX; 300 break; 301 case View.SCROLLBAR_POSITION_LEFT: 302 left = -mThumbW + (mThumbW * alpha) / ScrollFade.ALPHA_MAX; 303 break; 304 } 305 mThumbDrawable.setBounds(left, 0, left + mThumbW, mThumbH); 306 mChangedBounds = true; 307 } 308 309 if (mTrackDrawable != null) { 310 final int left = mThumbDrawable.getBounds().left; 311 final int trackWidth = mTrackDrawable.getIntrinsicWidth(); 312 final int trackLeft = (left + mThumbW) / 2 - trackWidth / 2; 313 mTrackDrawable.setBounds(trackLeft, 0, trackLeft + trackWidth, mList.getHeight()); 314 mTrackDrawable.draw(canvas); 315 } 316 317 canvas.translate(0, y); 318 mThumbDrawable.draw(canvas); 319 canvas.translate(0, -y); 320 321 // If user is dragging the scroll bar, draw the alphabet overlay 322 if (mState == STATE_DRAGGING && mDrawOverlay) { 323 if (mOverlayPosition == OVERLAY_AT_THUMB) { 324 int left = 0; 325 switch (mPosition) { 326 default: 327 case View.SCROLLBAR_POSITION_DEFAULT: 328 case View.SCROLLBAR_POSITION_RIGHT: 329 left = Math.max(0, 330 mThumbDrawable.getBounds().left - mThumbW - mOverlaySize); 331 break; 332 case View.SCROLLBAR_POSITION_LEFT: 333 left = Math.min(mThumbDrawable.getBounds().right + mThumbW, 334 mList.getWidth() - mOverlaySize); 335 break; 336 } 337 338 int top = Math.max(0, 339 Math.min(y + (mThumbH - mOverlaySize) / 2, mList.getHeight() - mOverlaySize)); 340 341 final RectF pos = mOverlayPos; 342 pos.left = left; 343 pos.right = pos.left + mOverlaySize; 344 pos.top = top; 345 pos.bottom = pos.top + mOverlaySize; 346 if (mOverlayDrawable != null) { 347 mOverlayDrawable.setBounds((int) pos.left, (int) pos.top, 348 (int) pos.right, (int) pos.bottom); 349 } 350 } 351 mOverlayDrawable.draw(canvas); 352 final Paint paint = mPaint; 353 float descent = paint.descent(); 354 final RectF rectF = mOverlayPos; 355 final Rect tmpRect = mTmpRect; 356 mOverlayDrawable.getPadding(tmpRect); 357 final int hOff = (tmpRect.right - tmpRect.left) / 2; 358 final int vOff = (tmpRect.bottom - tmpRect.top) / 2; 359 canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2 - hOff, 360 (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent - vOff, 361 paint); 362 } else if (mState == STATE_EXIT) { 363 if (alpha == 0) { // Done with exit 364 setState(STATE_NONE); 365 } else { 366 mList.invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH); 367 } 368 } 369 } 370 371 void onSizeChanged(int w, int h, int oldw, int oldh) { 372 if (mThumbDrawable != null) { 373 switch (mPosition) { 374 default: 375 case View.SCROLLBAR_POSITION_DEFAULT: 376 case View.SCROLLBAR_POSITION_RIGHT: 377 mThumbDrawable.setBounds(w - mThumbW, 0, w, mThumbH); 378 break; 379 case View.SCROLLBAR_POSITION_LEFT: 380 mThumbDrawable.setBounds(0, 0, mThumbW, mThumbH); 381 break; 382 } 383 } 384 if (mOverlayPosition == OVERLAY_FLOATING) { 385 final RectF pos = mOverlayPos; 386 pos.left = (w - mOverlaySize) / 2; 387 pos.right = pos.left + mOverlaySize; 388 pos.top = h / 10; // 10% from top 389 pos.bottom = pos.top + mOverlaySize; 390 if (mOverlayDrawable != null) { 391 mOverlayDrawable.setBounds((int) pos.left, (int) pos.top, 392 (int) pos.right, (int) pos.bottom); 393 } 394 } 395 } 396 397 void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 398 int totalItemCount) { 399 // Are there enough pages to require fast scroll? Recompute only if total count changes 400 if (mItemCount != totalItemCount && visibleItemCount > 0) { 401 mItemCount = totalItemCount; 402 mLongList = mItemCount / visibleItemCount >= MIN_PAGES; 403 } 404 if (!mLongList) { 405 if (mState != STATE_NONE) { 406 setState(STATE_NONE); 407 } 408 return; 409 } 410 if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING ) { 411 mThumbY = ((mList.getHeight() - mThumbH) * firstVisibleItem) 412 / (totalItemCount - visibleItemCount); 413 if (mChangedBounds) { 414 resetThumbPos(); 415 mChangedBounds = false; 416 } 417 } 418 mScrollCompleted = true; 419 if (firstVisibleItem == mVisibleItem) { 420 return; 421 } 422 mVisibleItem = firstVisibleItem; 423 if (mState != STATE_DRAGGING) { 424 setState(STATE_VISIBLE); 425 if (!mAlwaysShow) { 426 mHandler.postDelayed(mScrollFade, FADE_TIMEOUT); 427 } 428 } 429 } 430 431 SectionIndexer getSectionIndexer() { 432 return mSectionIndexer; 433 } 434 435 Object[] getSections() { 436 if (mListAdapter == null && mList != null) { 437 getSectionsFromIndexer(); 438 } 439 return mSections; 440 } 441 442 private void getSectionsFromIndexer() { 443 Adapter adapter = mList.getAdapter(); 444 mSectionIndexer = null; 445 if (adapter instanceof HeaderViewListAdapter) { 446 mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount(); 447 adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter(); 448 } 449 if (adapter instanceof ExpandableListConnector) { 450 ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter(); 451 if (expAdapter instanceof SectionIndexer) { 452 mSectionIndexer = (SectionIndexer) expAdapter; 453 mListAdapter = (BaseAdapter) adapter; 454 mSections = mSectionIndexer.getSections(); 455 } 456 } else { 457 if (adapter instanceof SectionIndexer) { 458 mListAdapter = (BaseAdapter) adapter; 459 mSectionIndexer = (SectionIndexer) adapter; 460 mSections = mSectionIndexer.getSections(); 461 462 } else { 463 mListAdapter = (BaseAdapter) adapter; 464 mSections = new String[] { " " }; 465 } 466 } 467 } 468 469 private void scrollTo(float position) { 470 int count = mList.getCount(); 471 mScrollCompleted = false; 472 float fThreshold = (1.0f / count) / 8; 473 final Object[] sections = mSections; 474 int sectionIndex; 475 if (sections != null && sections.length > 1) { 476 final int nSections = sections.length; 477 int section = (int) (position * nSections); 478 if (section >= nSections) { 479 section = nSections - 1; 480 } 481 int exactSection = section; 482 sectionIndex = section; 483 int index = mSectionIndexer.getPositionForSection(section); 484 // Given the expected section and index, the following code will 485 // try to account for missing sections (no names starting with..) 486 // It will compute the scroll space of surrounding empty sections 487 // and interpolate the currently visible letter's range across the 488 // available space, so that there is always some list movement while 489 // the user moves the thumb. 490 int nextIndex = count; 491 int prevIndex = index; 492 int prevSection = section; 493 int nextSection = section + 1; 494 // Assume the next section is unique 495 if (section < nSections - 1) { 496 nextIndex = mSectionIndexer.getPositionForSection(section + 1); 497 } 498 499 // Find the previous index if we're slicing the previous section 500 if (nextIndex == index) { 501 // Non-existent letter 502 while (section > 0) { 503 section--; 504 prevIndex = mSectionIndexer.getPositionForSection(section); 505 if (prevIndex != index) { 506 prevSection = section; 507 sectionIndex = section; 508 break; 509 } else if (section == 0) { 510 // When section reaches 0 here, sectionIndex must follow it. 511 // Assuming mSectionIndexer.getPositionForSection(0) == 0. 512 sectionIndex = 0; 513 break; 514 } 515 } 516 } 517 // Find the next index, in case the assumed next index is not 518 // unique. For instance, if there is no P, then request for P's 519 // position actually returns Q's. So we need to look ahead to make 520 // sure that there is really a Q at Q's position. If not, move 521 // further down... 522 int nextNextSection = nextSection + 1; 523 while (nextNextSection < nSections && 524 mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) { 525 nextNextSection++; 526 nextSection++; 527 } 528 // Compute the beginning and ending scroll range percentage of the 529 // currently visible letter. This could be equal to or greater than 530 // (1 / nSections). 531 float fPrev = (float) prevSection / nSections; 532 float fNext = (float) nextSection / nSections; 533 if (prevSection == exactSection && position - fPrev < fThreshold) { 534 index = prevIndex; 535 } else { 536 index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev) 537 / (fNext - fPrev)); 538 } 539 // Don't overflow 540 if (index > count - 1) index = count - 1; 541 542 if (mList instanceof ExpandableListView) { 543 ExpandableListView expList = (ExpandableListView) mList; 544 expList.setSelectionFromTop(expList.getFlatListPosition( 545 ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0); 546 } else if (mList instanceof ListView) { 547 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0); 548 } else { 549 mList.setSelection(index + mListOffset); 550 } 551 } else { 552 int index = (int) (position * count); 553 if (mList instanceof ExpandableListView) { 554 ExpandableListView expList = (ExpandableListView) mList; 555 expList.setSelectionFromTop(expList.getFlatListPosition( 556 ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0); 557 } else if (mList instanceof ListView) { 558 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0); 559 } else { 560 mList.setSelection(index + mListOffset); 561 } 562 sectionIndex = -1; 563 } 564 565 if (sectionIndex >= 0) { 566 String text = mSectionText = sections[sectionIndex].toString(); 567 mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') && 568 sectionIndex < sections.length; 569 } else { 570 mDrawOverlay = false; 571 } 572 } 573 574 private void cancelFling() { 575 // Cancel the list fling 576 MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0); 577 mList.onTouchEvent(cancelFling); 578 cancelFling.recycle(); 579 } 580 581 boolean onInterceptTouchEvent(MotionEvent ev) { 582 if (mState > STATE_NONE && ev.getAction() == MotionEvent.ACTION_DOWN) { 583 if (isPointInside(ev.getX(), ev.getY())) { 584 setState(STATE_DRAGGING); 585 return true; 586 } 587 } 588 return false; 589 } 590 591 boolean onTouchEvent(MotionEvent me) { 592 if (mState == STATE_NONE) { 593 return false; 594 } 595 596 final int action = me.getAction(); 597 598 if (action == MotionEvent.ACTION_DOWN) { 599 if (isPointInside(me.getX(), me.getY())) { 600 setState(STATE_DRAGGING); 601 if (mListAdapter == null && mList != null) { 602 getSectionsFromIndexer(); 603 } 604 if (mList != null) { 605 mList.requestDisallowInterceptTouchEvent(true); 606 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 607 } 608 609 cancelFling(); 610 return true; 611 } 612 } else if (action == MotionEvent.ACTION_UP) { // don't add ACTION_CANCEL here 613 if (mState == STATE_DRAGGING) { 614 if (mList != null) { 615 // ViewGroup does the right thing already, but there might 616 // be other classes that don't properly reset on touch-up, 617 // so do this explicitly just in case. 618 mList.requestDisallowInterceptTouchEvent(false); 619 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 620 } 621 setState(STATE_VISIBLE); 622 final Handler handler = mHandler; 623 handler.removeCallbacks(mScrollFade); 624 if (!mAlwaysShow) { 625 handler.postDelayed(mScrollFade, 1000); 626 } 627 628 mList.invalidate(); 629 return true; 630 } 631 } else if (action == MotionEvent.ACTION_MOVE) { 632 if (mState == STATE_DRAGGING) { 633 final int viewHeight = mList.getHeight(); 634 // Jitter 635 int newThumbY = (int) me.getY() - mThumbH + 10; 636 if (newThumbY < 0) { 637 newThumbY = 0; 638 } else if (newThumbY + mThumbH > viewHeight) { 639 newThumbY = viewHeight - mThumbH; 640 } 641 if (Math.abs(mThumbY - newThumbY) < 2) { 642 return true; 643 } 644 mThumbY = newThumbY; 645 // If the previous scrollTo is still pending 646 if (mScrollCompleted) { 647 scrollTo((float) mThumbY / (viewHeight - mThumbH)); 648 } 649 return true; 650 } 651 } 652 return false; 653 } 654 655 boolean isPointInside(float x, float y) { 656 boolean inTrack = false; 657 switch (mPosition) { 658 default: 659 case View.SCROLLBAR_POSITION_DEFAULT: 660 case View.SCROLLBAR_POSITION_RIGHT: 661 inTrack = x > mList.getWidth() - mThumbW; 662 break; 663 case View.SCROLLBAR_POSITION_LEFT: 664 inTrack = x < mThumbW; 665 break; 666 } 667 668 // Allow taps in the track to start moving. 669 return inTrack && (mTrackDrawable != null || y >= mThumbY && y <= mThumbY + mThumbH); 670 } 671 672 public class ScrollFade implements Runnable { 673 674 long mStartTime; 675 long mFadeDuration; 676 static final int ALPHA_MAX = 208; 677 static final long FADE_DURATION = 200; 678 679 void startFade() { 680 mFadeDuration = FADE_DURATION; 681 mStartTime = SystemClock.uptimeMillis(); 682 setState(STATE_EXIT); 683 } 684 685 int getAlpha() { 686 if (getState() != STATE_EXIT) { 687 return ALPHA_MAX; 688 } 689 int alpha; 690 long now = SystemClock.uptimeMillis(); 691 if (now > mStartTime + mFadeDuration) { 692 alpha = 0; 693 } else { 694 alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration); 695 } 696 return alpha; 697 } 698 699 public void run() { 700 if (getState() != STATE_EXIT) { 701 startFade(); 702 return; 703 } 704 705 if (getAlpha() > 0) { 706 mList.invalidate(); 707 } else { 708 setState(STATE_NONE); 709 } 710 } 711 } 712} 713