FastScroller.java revision 4f434c7fb40b75d94e3ca2e4a905808ca34296e5
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.animation.Animator; 20import android.animation.Animator.AnimatorListener; 21import android.animation.AnimatorListenerAdapter; 22import android.animation.AnimatorSet; 23import android.animation.ObjectAnimator; 24import android.animation.PropertyValuesHolder; 25import android.content.Context; 26import android.content.res.ColorStateList; 27import android.content.res.TypedArray; 28import android.graphics.Rect; 29import android.graphics.drawable.Drawable; 30import android.os.Build; 31import android.os.SystemClock; 32import android.text.TextUtils; 33import android.text.TextUtils.TruncateAt; 34import android.util.IntProperty; 35import android.util.MathUtils; 36import android.util.Property; 37import android.util.TypedValue; 38import android.view.Gravity; 39import android.view.MotionEvent; 40import android.view.View; 41import android.view.View.MeasureSpec; 42import android.view.ViewConfiguration; 43import android.view.ViewGroup.LayoutParams; 44import android.view.ViewGroupOverlay; 45import android.widget.AbsListView.OnScrollListener; 46import android.widget.ImageView.ScaleType; 47 48/** 49 * Helper class for AbsListView to draw and control the Fast Scroll thumb 50 */ 51class FastScroller { 52 /** Duration of fade-out animation. */ 53 private static final int DURATION_FADE_OUT = 300; 54 55 /** Duration of fade-in animation. */ 56 private static final int DURATION_FADE_IN = 150; 57 58 /** Duration of transition cross-fade animation. */ 59 private static final int DURATION_CROSS_FADE = 50; 60 61 /** Duration of transition resize animation. */ 62 private static final int DURATION_RESIZE = 100; 63 64 /** Inactivity timeout before fading controls. */ 65 private static final long FADE_TIMEOUT = 1500; 66 67 /** Minimum number of pages to justify showing a fast scroll thumb. */ 68 private static final int MIN_PAGES = 4; 69 70 /** Scroll thumb and preview not showing. */ 71 private static final int STATE_NONE = 0; 72 73 /** Scroll thumb visible and moving along with the scrollbar. */ 74 private static final int STATE_VISIBLE = 1; 75 76 /** Scroll thumb and preview being dragged by user. */ 77 private static final int STATE_DRAGGING = 2; 78 79 // Positions for preview image and text. 80 private static final int OVERLAY_FLOATING = 0; 81 private static final int OVERLAY_AT_THUMB = 1; 82 83 // Indices for mPreviewResId. 84 private static final int PREVIEW_LEFT = 0; 85 private static final int PREVIEW_RIGHT = 1; 86 87 /** Delay before considering a tap in the thumb area to be a drag. */ 88 private static final long TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); 89 90 private final Rect mTempBounds = new Rect(); 91 private final Rect mTempMargins = new Rect(); 92 private final Rect mContainerRect = new Rect(); 93 94 private final AbsListView mList; 95 private final ViewGroupOverlay mOverlay; 96 private final TextView mPrimaryText; 97 private final TextView mSecondaryText; 98 private final ImageView mThumbImage; 99 private final ImageView mTrackImage; 100 private final View mPreviewImage; 101 102 /** 103 * Preview image resource IDs for left- and right-aligned layouts. See 104 * {@link #PREVIEW_LEFT} and {@link #PREVIEW_RIGHT}. 105 */ 106 private final int[] mPreviewResId = new int[2]; 107 108 /** 109 * Padding in pixels around the preview text. Applied as layout margins to 110 * the preview text and padding to the preview image. 111 */ 112 private int mPreviewPadding; 113 114 private int mPreviewMinWidth; 115 private int mPreviewMinHeight; 116 private int mThumbMinWidth; 117 private int mThumbMinHeight; 118 119 /** Theme-specified text size. Used only if text appearance is not set. */ 120 private float mTextSize; 121 122 /** Theme-specified text color. Used only if text appearance is not set. */ 123 private ColorStateList mTextColor; 124 125 private Drawable mThumbDrawable; 126 private Drawable mTrackDrawable; 127 private int mTextAppearance; 128 129 /** Total width of decorations. */ 130 private int mWidth; 131 132 /** Set containing decoration transition animations. */ 133 private AnimatorSet mDecorAnimation; 134 135 /** Set containing preview text transition animations. */ 136 private AnimatorSet mPreviewAnimation; 137 138 /** Whether the primary text is showing. */ 139 private boolean mShowingPrimary; 140 141 /** Whether we're waiting for completion of scrollTo(). */ 142 private boolean mScrollCompleted; 143 144 /** The position of the first visible item in the list. */ 145 private int mFirstVisibleItem; 146 147 /** The number of headers at the top of the view. */ 148 private int mHeaderCount; 149 150 /** The index of the current section. */ 151 private int mCurrentSection = -1; 152 153 /** The current scrollbar position. */ 154 private int mScrollbarPosition = -1; 155 156 /** Whether the list is long enough to need a fast scroller. */ 157 private boolean mLongList; 158 159 private Object[] mSections; 160 161 /** Whether this view is currently performing layout. */ 162 private boolean mUpdatingLayout; 163 164 /** 165 * Current decoration state, one of: 166 * <ul> 167 * <li>{@link #STATE_NONE}, nothing visible 168 * <li>{@link #STATE_VISIBLE}, showing track and thumb 169 * <li>{@link #STATE_DRAGGING}, visible and showing preview 170 * </ul> 171 */ 172 private int mState; 173 174 /** Whether the preview image is visible. */ 175 private boolean mShowingPreview; 176 177 private Adapter mListAdapter; 178 private SectionIndexer mSectionIndexer; 179 180 /** Whether decorations should be laid out from right to left. */ 181 private boolean mLayoutFromRight; 182 183 /** Whether the fast scroller is enabled. */ 184 private boolean mEnabled; 185 186 /** Whether the scrollbar and decorations should always be shown. */ 187 private boolean mAlwaysShow; 188 189 /** 190 * Position for the preview image and text. One of: 191 * <ul> 192 * <li>{@link #OVERLAY_AT_THUMB} 193 * <li>{@link #OVERLAY_FLOATING} 194 * </ul> 195 */ 196 private int mOverlayPosition; 197 198 /** Current scrollbar style, including inset and overlay properties. */ 199 private int mScrollBarStyle; 200 201 /** Whether to precisely match the thumb position to the list. */ 202 private boolean mMatchDragPosition; 203 204 private float mInitialTouchY; 205 private long mPendingDrag = -1; 206 private int mScaledTouchSlop; 207 208 /** 209 * Used to delay hiding fast scroll decorations. 210 */ 211 private final Runnable mDeferHide = new Runnable() { 212 @Override 213 public void run() { 214 setState(STATE_NONE); 215 } 216 }; 217 218 /** 219 * Used to effect a transition from primary to secondary text. 220 */ 221 private final AnimatorListener mSwitchPrimaryListener = new AnimatorListenerAdapter() { 222 @Override 223 public void onAnimationEnd(Animator animation) { 224 mShowingPrimary = !mShowingPrimary; 225 } 226 }; 227 228 public FastScroller(AbsListView listView, int styleResId) { 229 mList = listView; 230 231 final Context context = listView.getContext(); 232 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 233 mScrollBarStyle = listView.getScrollBarStyle(); 234 235 mScrollCompleted = true; 236 mState = STATE_VISIBLE; 237 mMatchDragPosition = 238 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB; 239 240 mTrackImage = new ImageView(context); 241 mTrackImage.setScaleType(ScaleType.FIT_XY); 242 mThumbImage = new ImageView(context); 243 mThumbImage.setScaleType(ScaleType.FIT_XY); 244 mPreviewImage = new View(context); 245 mPreviewImage.setAlpha(0f); 246 247 mPrimaryText = createPreviewTextView(context); 248 mSecondaryText = createPreviewTextView(context); 249 250 setStyle(styleResId); 251 252 final ViewGroupOverlay overlay = listView.getOverlay(); 253 mOverlay = overlay; 254 overlay.add(mTrackImage); 255 overlay.add(mThumbImage); 256 overlay.add(mPreviewImage); 257 overlay.add(mPrimaryText); 258 overlay.add(mSecondaryText); 259 260 getSectionsFromIndexer(); 261 updateLongList(listView.getChildCount(), listView.getCount()); 262 setScrollbarPosition(listView.getVerticalScrollbarPosition()); 263 postAutoHide(); 264 } 265 266 private void updateAppearance() { 267 final Context context = mList.getContext(); 268 int width = 0; 269 270 // Add track to overlay if it has an image. 271 mTrackImage.setImageDrawable(mTrackDrawable); 272 if (mTrackDrawable != null) { 273 width = Math.max(width, mTrackDrawable.getIntrinsicWidth()); 274 } 275 276 // Add thumb to overlay if it has an image. 277 mThumbImage.setImageDrawable(mThumbDrawable); 278 mThumbImage.setMinimumWidth(mThumbMinWidth); 279 mThumbImage.setMinimumHeight(mThumbMinHeight); 280 if (mThumbDrawable != null) { 281 width = Math.max(width, mThumbDrawable.getIntrinsicWidth()); 282 } 283 284 // Account for minimum thumb width. 285 mWidth = Math.max(width, mThumbMinWidth); 286 287 mPreviewImage.setMinimumWidth(mPreviewMinWidth); 288 mPreviewImage.setMinimumHeight(mPreviewMinHeight); 289 290 if (mTextAppearance != 0) { 291 mPrimaryText.setTextAppearance(context, mTextAppearance); 292 mSecondaryText.setTextAppearance(context, mTextAppearance); 293 } 294 295 if (mTextColor != null) { 296 mPrimaryText.setTextColor(mTextColor); 297 mSecondaryText.setTextColor(mTextColor); 298 } 299 300 if (mTextSize > 0) { 301 mPrimaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); 302 mSecondaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); 303 } 304 305 final int textMinSize = Math.max(0, mPreviewMinHeight); 306 mPrimaryText.setMinimumWidth(textMinSize); 307 mPrimaryText.setMinimumHeight(textMinSize); 308 mSecondaryText.setMinimumWidth(textMinSize); 309 mSecondaryText.setMinimumHeight(textMinSize); 310 311 refreshDrawablePressedState(); 312 } 313 314 public void setStyle(int resId) { 315 final Context context = mList.getContext(); 316 final TypedArray ta = context.obtainStyledAttributes(null, 317 com.android.internal.R.styleable.FastScroll, android.R.attr.fastScrollStyle, resId); 318 final int N = ta.getIndexCount(); 319 for (int i = 0; i < N; i++) { 320 final int index = ta.getIndex(i); 321 switch (index) { 322 case com.android.internal.R.styleable.FastScroll_position: 323 mOverlayPosition = ta.getInt(index, OVERLAY_FLOATING); 324 break; 325 case com.android.internal.R.styleable.FastScroll_backgroundLeft: 326 mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(index, 0); 327 break; 328 case com.android.internal.R.styleable.FastScroll_backgroundRight: 329 mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(index, 0); 330 break; 331 case com.android.internal.R.styleable.FastScroll_thumbDrawable: 332 mThumbDrawable = ta.getDrawable(index); 333 break; 334 case com.android.internal.R.styleable.FastScroll_trackDrawable: 335 mTrackDrawable = ta.getDrawable(index); 336 break; 337 case com.android.internal.R.styleable.FastScroll_textAppearance: 338 mTextAppearance = ta.getResourceId(index, 0); 339 break; 340 case com.android.internal.R.styleable.FastScroll_textColor: 341 mTextColor = ta.getColorStateList(index); 342 break; 343 case com.android.internal.R.styleable.FastScroll_textSize: 344 mTextSize = ta.getDimensionPixelSize(index, 0); 345 break; 346 case com.android.internal.R.styleable.FastScroll_minWidth: 347 mPreviewMinWidth = ta.getDimensionPixelSize(index, 0); 348 break; 349 case com.android.internal.R.styleable.FastScroll_minHeight: 350 mPreviewMinHeight = ta.getDimensionPixelSize(index, 0); 351 break; 352 case com.android.internal.R.styleable.FastScroll_thumbMinWidth: 353 mThumbMinWidth = ta.getDimensionPixelSize(index, 0); 354 break; 355 case com.android.internal.R.styleable.FastScroll_thumbMinHeight: 356 mThumbMinHeight = ta.getDimensionPixelSize(index, 0); 357 break; 358 case com.android.internal.R.styleable.FastScroll_padding: 359 mPreviewPadding = ta.getDimensionPixelSize(index, 0); 360 break; 361 } 362 } 363 364 updateAppearance(); 365 } 366 367 /** 368 * Removes this FastScroller overlay from the host view. 369 */ 370 public void remove() { 371 mOverlay.remove(mTrackImage); 372 mOverlay.remove(mThumbImage); 373 mOverlay.remove(mPreviewImage); 374 mOverlay.remove(mPrimaryText); 375 mOverlay.remove(mSecondaryText); 376 } 377 378 /** 379 * @param enabled Whether the fast scroll thumb is enabled. 380 */ 381 public void setEnabled(boolean enabled) { 382 if (mEnabled != enabled) { 383 mEnabled = enabled; 384 385 onStateDependencyChanged(); 386 } 387 } 388 389 /** 390 * @return Whether the fast scroll thumb is enabled. 391 */ 392 public boolean isEnabled() { 393 return mEnabled && (mLongList || mAlwaysShow); 394 } 395 396 /** 397 * @param alwaysShow Whether the fast scroll thumb should always be shown 398 */ 399 public void setAlwaysShow(boolean alwaysShow) { 400 if (mAlwaysShow != alwaysShow) { 401 mAlwaysShow = alwaysShow; 402 403 onStateDependencyChanged(); 404 } 405 } 406 407 /** 408 * @return Whether the fast scroll thumb will always be shown 409 * @see #setAlwaysShow(boolean) 410 */ 411 public boolean isAlwaysShowEnabled() { 412 return mAlwaysShow; 413 } 414 415 /** 416 * Called when one of the variables affecting enabled state changes. 417 */ 418 private void onStateDependencyChanged() { 419 if (isEnabled()) { 420 if (isAlwaysShowEnabled()) { 421 setState(STATE_VISIBLE); 422 } else if (mState == STATE_VISIBLE) { 423 postAutoHide(); 424 } 425 } else { 426 stop(); 427 } 428 429 mList.resolvePadding(); 430 } 431 432 public void setScrollBarStyle(int style) { 433 if (mScrollBarStyle != style) { 434 mScrollBarStyle = style; 435 436 updateLayout(); 437 } 438 } 439 440 /** 441 * Immediately transitions the fast scroller decorations to a hidden state. 442 */ 443 public void stop() { 444 setState(STATE_NONE); 445 } 446 447 public void setScrollbarPosition(int position) { 448 if (position == View.SCROLLBAR_POSITION_DEFAULT) { 449 position = mList.isLayoutRtl() ? 450 View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT; 451 } 452 453 if (mScrollbarPosition != position) { 454 mScrollbarPosition = position; 455 mLayoutFromRight = position != View.SCROLLBAR_POSITION_LEFT; 456 457 final int previewResId = mPreviewResId[mLayoutFromRight ? PREVIEW_RIGHT : PREVIEW_LEFT]; 458 mPreviewImage.setBackgroundResource(previewResId); 459 460 // Add extra padding for text. 461 final Drawable background = mPreviewImage.getBackground(); 462 if (background != null) { 463 final Rect padding = mTempBounds; 464 background.getPadding(padding); 465 padding.offset(mPreviewPadding, mPreviewPadding); 466 mPreviewImage.setPadding(padding.left, padding.top, padding.right, padding.bottom); 467 } 468 469 // Requires re-layout. 470 updateLayout(); 471 } 472 } 473 474 public int getWidth() { 475 return mWidth; 476 } 477 478 public void onSizeChanged(int w, int h, int oldw, int oldh) { 479 updateLayout(); 480 } 481 482 public void onItemCountChanged(int totalItemCount) { 483 final int visibleItemCount = mList.getChildCount(); 484 final boolean hasMoreItems = totalItemCount - visibleItemCount > 0; 485 if (hasMoreItems && mState != STATE_DRAGGING) { 486 final int firstVisibleItem = mList.getFirstVisiblePosition(); 487 setThumbPos(getPosFromItemCount(firstVisibleItem, visibleItemCount, totalItemCount)); 488 } 489 490 updateLongList(visibleItemCount, totalItemCount); 491 } 492 493 private void updateLongList(int visibleItemCount, int totalItemCount) { 494 final boolean longList = visibleItemCount > 0 495 && totalItemCount / visibleItemCount >= MIN_PAGES; 496 if (mLongList != longList) { 497 mLongList = longList; 498 499 onStateDependencyChanged(); 500 } 501 } 502 503 /** 504 * Creates a view into which preview text can be placed. 505 */ 506 private TextView createPreviewTextView(Context context) { 507 final LayoutParams params = new LayoutParams( 508 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 509 final TextView textView = new TextView(context); 510 textView.setLayoutParams(params); 511 textView.setSingleLine(true); 512 textView.setEllipsize(TruncateAt.MIDDLE); 513 textView.setGravity(Gravity.CENTER); 514 textView.setAlpha(0f); 515 516 // Manually propagate inherited layout direction. 517 textView.setLayoutDirection(mList.getLayoutDirection()); 518 519 return textView; 520 } 521 522 /** 523 * Measures and layouts the scrollbar and decorations. 524 */ 525 public void updateLayout() { 526 // Prevent re-entry when RTL properties change as a side-effect of 527 // resolving padding. 528 if (mUpdatingLayout) { 529 return; 530 } 531 532 mUpdatingLayout = true; 533 534 updateContainerRect(); 535 536 layoutThumb(); 537 layoutTrack(); 538 539 final Rect bounds = mTempBounds; 540 measurePreview(mPrimaryText, bounds); 541 applyLayout(mPrimaryText, bounds); 542 measurePreview(mSecondaryText, bounds); 543 applyLayout(mSecondaryText, bounds); 544 545 if (mPreviewImage != null) { 546 // Apply preview image padding. 547 bounds.left -= mPreviewImage.getPaddingLeft(); 548 bounds.top -= mPreviewImage.getPaddingTop(); 549 bounds.right += mPreviewImage.getPaddingRight(); 550 bounds.bottom += mPreviewImage.getPaddingBottom(); 551 applyLayout(mPreviewImage, bounds); 552 } 553 554 mUpdatingLayout = false; 555 } 556 557 /** 558 * Layouts a view within the specified bounds and pins the pivot point to 559 * the appropriate edge. 560 * 561 * @param view The view to layout. 562 * @param bounds Bounds at which to layout the view. 563 */ 564 private void applyLayout(View view, Rect bounds) { 565 view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom); 566 view.setPivotX(mLayoutFromRight ? bounds.right - bounds.left : 0); 567 } 568 569 /** 570 * Measures the preview text bounds, taking preview image padding into 571 * account. This method should only be called after {@link #layoutThumb()} 572 * and {@link #layoutTrack()} have both been called at least once. 573 * 574 * @param v The preview text view to measure. 575 * @param out Rectangle into which measured bounds are placed. 576 */ 577 private void measurePreview(View v, Rect out) { 578 // Apply the preview image's padding as layout margins. 579 final Rect margins = mTempMargins; 580 margins.left = mPreviewImage.getPaddingLeft(); 581 margins.top = mPreviewImage.getPaddingTop(); 582 margins.right = mPreviewImage.getPaddingRight(); 583 margins.bottom = mPreviewImage.getPaddingBottom(); 584 585 if (mOverlayPosition == OVERLAY_AT_THUMB) { 586 measureViewToSide(v, mThumbImage, margins, out); 587 } else { 588 measureFloating(v, margins, out); 589 } 590 } 591 592 /** 593 * Measures the bounds for a view that should be laid out against the edge 594 * of an adjacent view. If no adjacent view is provided, lays out against 595 * the list edge. 596 * 597 * @param view The view to measure for layout. 598 * @param adjacent (Optional) The adjacent view, may be null to align to the 599 * list edge. 600 * @param margins Layout margins to apply to the view. 601 * @param out Rectangle into which measured bounds are placed. 602 */ 603 private void measureViewToSide(View view, View adjacent, Rect margins, Rect out) { 604 final int marginLeft; 605 final int marginTop; 606 final int marginRight; 607 if (margins == null) { 608 marginLeft = 0; 609 marginTop = 0; 610 marginRight = 0; 611 } else { 612 marginLeft = margins.left; 613 marginTop = margins.top; 614 marginRight = margins.right; 615 } 616 617 final Rect container = mContainerRect; 618 final int containerWidth = container.width(); 619 final int maxWidth; 620 if (adjacent == null) { 621 maxWidth = containerWidth; 622 } else if (mLayoutFromRight) { 623 maxWidth = adjacent.getLeft(); 624 } else { 625 maxWidth = containerWidth - adjacent.getRight(); 626 } 627 628 final int adjMaxWidth = maxWidth - marginLeft - marginRight; 629 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST); 630 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 631 view.measure(widthMeasureSpec, heightMeasureSpec); 632 633 // Align to the left or right. 634 final int width = Math.min(adjMaxWidth, view.getMeasuredWidth()); 635 final int left; 636 final int right; 637 if (mLayoutFromRight) { 638 right = (adjacent == null ? container.right : adjacent.getLeft()) - marginRight; 639 left = right - width; 640 } else { 641 left = (adjacent == null ? container.left : adjacent.getRight()) + marginLeft; 642 right = left + width; 643 } 644 645 // Don't adjust the vertical position. 646 final int top = marginTop; 647 final int bottom = top + view.getMeasuredHeight(); 648 out.set(left, top, right, bottom); 649 } 650 651 private void measureFloating(View preview, Rect margins, Rect out) { 652 final int marginLeft; 653 final int marginTop; 654 final int marginRight; 655 if (margins == null) { 656 marginLeft = 0; 657 marginTop = 0; 658 marginRight = 0; 659 } else { 660 marginLeft = margins.left; 661 marginTop = margins.top; 662 marginRight = margins.right; 663 } 664 665 final Rect container = mContainerRect; 666 final int containerWidth = container.width(); 667 final int adjMaxWidth = containerWidth - marginLeft - marginRight; 668 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST); 669 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 670 preview.measure(widthMeasureSpec, heightMeasureSpec); 671 672 // Align at the vertical center, 10% from the top. 673 final int containerHeight = container.height(); 674 final int width = preview.getMeasuredWidth(); 675 final int top = containerHeight / 10 + marginTop + container.top; 676 final int bottom = top + preview.getMeasuredHeight(); 677 final int left = (containerWidth - width) / 2 + container.left; 678 final int right = left + width; 679 out.set(left, top, right, bottom); 680 } 681 682 /** 683 * Updates the container rectangle used for layout. 684 */ 685 private void updateContainerRect() { 686 final AbsListView list = mList; 687 list.resolvePadding(); 688 689 final Rect container = mContainerRect; 690 container.left = 0; 691 container.top = 0; 692 container.right = list.getWidth(); 693 container.bottom = list.getHeight(); 694 695 final int scrollbarStyle = mScrollBarStyle; 696 if (scrollbarStyle == View.SCROLLBARS_INSIDE_INSET 697 || scrollbarStyle == View.SCROLLBARS_INSIDE_OVERLAY) { 698 container.left += list.getPaddingLeft(); 699 container.top += list.getPaddingTop(); 700 container.right -= list.getPaddingRight(); 701 container.bottom -= list.getPaddingBottom(); 702 703 // In inset mode, we need to adjust for padded scrollbar width. 704 if (scrollbarStyle == View.SCROLLBARS_INSIDE_INSET) { 705 final int width = getWidth(); 706 if (mScrollbarPosition == View.SCROLLBAR_POSITION_RIGHT) { 707 container.right += width; 708 } else { 709 container.left -= width; 710 } 711 } 712 } 713 } 714 715 /** 716 * Lays out the thumb according to the current scrollbar position. 717 */ 718 private void layoutThumb() { 719 final Rect bounds = mTempBounds; 720 measureViewToSide(mThumbImage, null, null, bounds); 721 applyLayout(mThumbImage, bounds); 722 } 723 724 /** 725 * Lays out the track centered on the thumb. Must be called after 726 * {@link #layoutThumb}. 727 */ 728 private void layoutTrack() { 729 final View track = mTrackImage; 730 final View thumb = mThumbImage; 731 final Rect container = mContainerRect; 732 final int containerWidth = container.width(); 733 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(containerWidth, MeasureSpec.AT_MOST); 734 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 735 track.measure(widthMeasureSpec, heightMeasureSpec); 736 737 final int trackWidth = track.getMeasuredWidth(); 738 final int thumbHalfHeight = thumb == null ? 0 : thumb.getHeight() / 2; 739 final int left = thumb.getLeft() + (thumb.getWidth() - trackWidth) / 2; 740 final int right = left + trackWidth; 741 final int top = container.top + thumbHalfHeight; 742 final int bottom = container.bottom - thumbHalfHeight; 743 track.layout(left, top, right, bottom); 744 } 745 746 private void setState(int state) { 747 mList.removeCallbacks(mDeferHide); 748 749 if (mAlwaysShow && state == STATE_NONE) { 750 state = STATE_VISIBLE; 751 } 752 753 if (state == mState) { 754 return; 755 } 756 757 switch (state) { 758 case STATE_NONE: 759 transitionToHidden(); 760 break; 761 case STATE_VISIBLE: 762 transitionToVisible(); 763 break; 764 case STATE_DRAGGING: 765 if (transitionPreviewLayout(mCurrentSection)) { 766 transitionToDragging(); 767 } else { 768 transitionToVisible(); 769 } 770 break; 771 } 772 773 mState = state; 774 775 refreshDrawablePressedState(); 776 } 777 778 private void refreshDrawablePressedState() { 779 final boolean isPressed = mState == STATE_DRAGGING; 780 mThumbImage.setPressed(isPressed); 781 mTrackImage.setPressed(isPressed); 782 } 783 784 /** 785 * Shows nothing. 786 */ 787 private void transitionToHidden() { 788 if (mDecorAnimation != null) { 789 mDecorAnimation.cancel(); 790 } 791 792 final Animator fadeOut = groupAnimatorOfFloat(View.ALPHA, 0f, mThumbImage, mTrackImage, 793 mPreviewImage, mPrimaryText, mSecondaryText).setDuration(DURATION_FADE_OUT); 794 795 // Push the thumb and track outside the list bounds. 796 final float offset = mLayoutFromRight ? mThumbImage.getWidth() : -mThumbImage.getWidth(); 797 final Animator slideOut = groupAnimatorOfFloat( 798 View.TRANSLATION_X, offset, mThumbImage, mTrackImage) 799 .setDuration(DURATION_FADE_OUT); 800 801 mDecorAnimation = new AnimatorSet(); 802 mDecorAnimation.playTogether(fadeOut, slideOut); 803 mDecorAnimation.start(); 804 805 mShowingPreview = false; 806 } 807 808 /** 809 * Shows the thumb and track. 810 */ 811 private void transitionToVisible() { 812 if (mDecorAnimation != null) { 813 mDecorAnimation.cancel(); 814 } 815 816 final Animator fadeIn = groupAnimatorOfFloat(View.ALPHA, 1f, mThumbImage, mTrackImage) 817 .setDuration(DURATION_FADE_IN); 818 final Animator fadeOut = groupAnimatorOfFloat( 819 View.ALPHA, 0f, mPreviewImage, mPrimaryText, mSecondaryText) 820 .setDuration(DURATION_FADE_OUT); 821 final Animator slideIn = groupAnimatorOfFloat( 822 View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN); 823 824 mDecorAnimation = new AnimatorSet(); 825 mDecorAnimation.playTogether(fadeIn, fadeOut, slideIn); 826 mDecorAnimation.start(); 827 828 mShowingPreview = false; 829 } 830 831 /** 832 * Shows the thumb, preview, and track. 833 */ 834 private void transitionToDragging() { 835 if (mDecorAnimation != null) { 836 mDecorAnimation.cancel(); 837 } 838 839 final Animator fadeIn = groupAnimatorOfFloat( 840 View.ALPHA, 1f, mThumbImage, mTrackImage, mPreviewImage) 841 .setDuration(DURATION_FADE_IN); 842 final Animator slideIn = groupAnimatorOfFloat( 843 View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN); 844 845 mDecorAnimation = new AnimatorSet(); 846 mDecorAnimation.playTogether(fadeIn, slideIn); 847 mDecorAnimation.start(); 848 849 mShowingPreview = true; 850 } 851 852 private void postAutoHide() { 853 mList.removeCallbacks(mDeferHide); 854 mList.postDelayed(mDeferHide, FADE_TIMEOUT); 855 } 856 857 public void onScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) { 858 if (!isEnabled()) { 859 setState(STATE_NONE); 860 return; 861 } 862 863 final boolean hasMoreItems = totalItemCount - visibleItemCount > 0; 864 if (hasMoreItems && mState != STATE_DRAGGING) { 865 setThumbPos(getPosFromItemCount(firstVisibleItem, visibleItemCount, totalItemCount)); 866 } 867 868 mScrollCompleted = true; 869 870 if (mFirstVisibleItem != firstVisibleItem) { 871 mFirstVisibleItem = firstVisibleItem; 872 873 // Show the thumb, if necessary, and set up auto-fade. 874 if (mState != STATE_DRAGGING) { 875 setState(STATE_VISIBLE); 876 postAutoHide(); 877 } 878 } 879 } 880 881 private void getSectionsFromIndexer() { 882 mSectionIndexer = null; 883 884 Adapter adapter = mList.getAdapter(); 885 if (adapter instanceof HeaderViewListAdapter) { 886 mHeaderCount = ((HeaderViewListAdapter) adapter).getHeadersCount(); 887 adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter(); 888 } 889 890 if (adapter instanceof ExpandableListConnector) { 891 final ExpandableListAdapter expAdapter = ((ExpandableListConnector) adapter) 892 .getAdapter(); 893 if (expAdapter instanceof SectionIndexer) { 894 mSectionIndexer = (SectionIndexer) expAdapter; 895 mListAdapter = adapter; 896 mSections = mSectionIndexer.getSections(); 897 } 898 } else if (adapter instanceof SectionIndexer) { 899 mListAdapter = adapter; 900 mSectionIndexer = (SectionIndexer) adapter; 901 mSections = mSectionIndexer.getSections(); 902 } else { 903 mListAdapter = adapter; 904 mSections = null; 905 } 906 } 907 908 public void onSectionsChanged() { 909 mListAdapter = null; 910 } 911 912 /** 913 * Scrolls to a specific position within the section 914 * @param position 915 */ 916 private void scrollTo(float position) { 917 mScrollCompleted = false; 918 919 final int count = mList.getCount(); 920 final Object[] sections = mSections; 921 final int sectionCount = sections == null ? 0 : sections.length; 922 int sectionIndex; 923 if (sections != null && sectionCount > 1) { 924 final int exactSection = MathUtils.constrain( 925 (int) (position * sectionCount), 0, sectionCount - 1); 926 int targetSection = exactSection; 927 int targetIndex = mSectionIndexer.getPositionForSection(targetSection); 928 sectionIndex = targetSection; 929 930 // Given the expected section and index, the following code will 931 // try to account for missing sections (no names starting with..) 932 // It will compute the scroll space of surrounding empty sections 933 // and interpolate the currently visible letter's range across the 934 // available space, so that there is always some list movement while 935 // the user moves the thumb. 936 int nextIndex = count; 937 int prevIndex = targetIndex; 938 int prevSection = targetSection; 939 int nextSection = targetSection + 1; 940 941 // Assume the next section is unique 942 if (targetSection < sectionCount - 1) { 943 nextIndex = mSectionIndexer.getPositionForSection(targetSection + 1); 944 } 945 946 // Find the previous index if we're slicing the previous section 947 if (nextIndex == targetIndex) { 948 // Non-existent letter 949 while (targetSection > 0) { 950 targetSection--; 951 prevIndex = mSectionIndexer.getPositionForSection(targetSection); 952 if (prevIndex != targetIndex) { 953 prevSection = targetSection; 954 sectionIndex = targetSection; 955 break; 956 } else if (targetSection == 0) { 957 // When section reaches 0 here, sectionIndex must follow it. 958 // Assuming mSectionIndexer.getPositionForSection(0) == 0. 959 sectionIndex = 0; 960 break; 961 } 962 } 963 } 964 965 // Find the next index, in case the assumed next index is not 966 // unique. For instance, if there is no P, then request for P's 967 // position actually returns Q's. So we need to look ahead to make 968 // sure that there is really a Q at Q's position. If not, move 969 // further down... 970 int nextNextSection = nextSection + 1; 971 while (nextNextSection < sectionCount && 972 mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) { 973 nextNextSection++; 974 nextSection++; 975 } 976 977 // Compute the beginning and ending scroll range percentage of the 978 // currently visible section. This could be equal to or greater than 979 // (1 / nSections). If the target position is near the previous 980 // position, snap to the previous position. 981 final float prevPosition = (float) prevSection / sectionCount; 982 final float nextPosition = (float) nextSection / sectionCount; 983 final float snapThreshold = (count == 0) ? Float.MAX_VALUE : .125f / count; 984 if (prevSection == exactSection && position - prevPosition < snapThreshold) { 985 targetIndex = prevIndex; 986 } else { 987 targetIndex = prevIndex + (int) ((nextIndex - prevIndex) * (position - prevPosition) 988 / (nextPosition - prevPosition)); 989 } 990 991 // Clamp to valid positions. 992 targetIndex = MathUtils.constrain(targetIndex, 0, count - 1); 993 994 if (mList instanceof ExpandableListView) { 995 final ExpandableListView expList = (ExpandableListView) mList; 996 expList.setSelectionFromTop(expList.getFlatListPosition( 997 ExpandableListView.getPackedPositionForGroup(targetIndex + mHeaderCount)), 998 0); 999 } else if (mList instanceof ListView) { 1000 ((ListView) mList).setSelectionFromTop(targetIndex + mHeaderCount, 0); 1001 } else { 1002 mList.setSelection(targetIndex + mHeaderCount); 1003 } 1004 } else { 1005 final int index = MathUtils.constrain((int) (position * count), 0, count - 1); 1006 1007 if (mList instanceof ExpandableListView) { 1008 ExpandableListView expList = (ExpandableListView) mList; 1009 expList.setSelectionFromTop(expList.getFlatListPosition( 1010 ExpandableListView.getPackedPositionForGroup(index + mHeaderCount)), 0); 1011 } else if (mList instanceof ListView) { 1012 ((ListView)mList).setSelectionFromTop(index + mHeaderCount, 0); 1013 } else { 1014 mList.setSelection(index + mHeaderCount); 1015 } 1016 1017 sectionIndex = -1; 1018 } 1019 1020 if (mCurrentSection != sectionIndex) { 1021 mCurrentSection = sectionIndex; 1022 1023 final boolean hasPreview = transitionPreviewLayout(sectionIndex); 1024 if (!mShowingPreview && hasPreview) { 1025 transitionToDragging(); 1026 } else if (mShowingPreview && !hasPreview) { 1027 transitionToVisible(); 1028 } 1029 } 1030 } 1031 1032 /** 1033 * Transitions the preview text to a new section. Handles animation, 1034 * measurement, and layout. If the new preview text is empty, returns false. 1035 * 1036 * @param sectionIndex The section index to which the preview should 1037 * transition. 1038 * @return False if the new preview text is empty. 1039 */ 1040 private boolean transitionPreviewLayout(int sectionIndex) { 1041 final Object[] sections = mSections; 1042 String text = null; 1043 if (sections != null && sectionIndex >= 0 && sectionIndex < sections.length) { 1044 final Object section = sections[sectionIndex]; 1045 if (section != null) { 1046 text = section.toString(); 1047 } 1048 } 1049 1050 final Rect bounds = mTempBounds; 1051 final View preview = mPreviewImage; 1052 final TextView showing; 1053 final TextView target; 1054 if (mShowingPrimary) { 1055 showing = mPrimaryText; 1056 target = mSecondaryText; 1057 } else { 1058 showing = mSecondaryText; 1059 target = mPrimaryText; 1060 } 1061 1062 // Set and layout target immediately. 1063 target.setText(text); 1064 measurePreview(target, bounds); 1065 applyLayout(target, bounds); 1066 1067 if (mPreviewAnimation != null) { 1068 mPreviewAnimation.cancel(); 1069 } 1070 1071 // Cross-fade preview text. 1072 final Animator showTarget = animateAlpha(target, 1f).setDuration(DURATION_CROSS_FADE); 1073 final Animator hideShowing = animateAlpha(showing, 0f).setDuration(DURATION_CROSS_FADE); 1074 hideShowing.addListener(mSwitchPrimaryListener); 1075 1076 // Apply preview image padding and animate bounds, if necessary. 1077 bounds.left -= preview.getPaddingLeft(); 1078 bounds.top -= preview.getPaddingTop(); 1079 bounds.right += preview.getPaddingRight(); 1080 bounds.bottom += preview.getPaddingBottom(); 1081 final Animator resizePreview = animateBounds(preview, bounds); 1082 resizePreview.setDuration(DURATION_RESIZE); 1083 1084 mPreviewAnimation = new AnimatorSet(); 1085 final AnimatorSet.Builder builder = mPreviewAnimation.play(hideShowing).with(showTarget); 1086 builder.with(resizePreview); 1087 1088 // The current preview size is unaffected by hidden or showing. It's 1089 // used to set starting scales for things that need to be scaled down. 1090 final int previewWidth = preview.getWidth() - preview.getPaddingLeft() 1091 - preview.getPaddingRight(); 1092 1093 // If target is too large, shrink it immediately to fit and expand to 1094 // target size. Otherwise, start at target size. 1095 final int targetWidth = target.getWidth(); 1096 if (targetWidth > previewWidth) { 1097 target.setScaleX((float) previewWidth / targetWidth); 1098 final Animator scaleAnim = animateScaleX(target, 1f).setDuration(DURATION_RESIZE); 1099 builder.with(scaleAnim); 1100 } else { 1101 target.setScaleX(1f); 1102 } 1103 1104 // If showing is larger than target, shrink to target size. 1105 final int showingWidth = showing.getWidth(); 1106 if (showingWidth > targetWidth) { 1107 final float scale = (float) targetWidth / showingWidth; 1108 final Animator scaleAnim = animateScaleX(showing, scale).setDuration(DURATION_RESIZE); 1109 builder.with(scaleAnim); 1110 } 1111 1112 mPreviewAnimation.start(); 1113 1114 return !TextUtils.isEmpty(text); 1115 } 1116 1117 /** 1118 * Positions the thumb and preview widgets. 1119 * 1120 * @param position The position, between 0 and 1, along the track at which 1121 * to place the thumb. 1122 */ 1123 private void setThumbPos(float position) { 1124 final Rect container = mContainerRect; 1125 final int top = container.top; 1126 final int bottom = container.bottom; 1127 1128 final View trackImage = mTrackImage; 1129 final View thumbImage = mThumbImage; 1130 final float min = trackImage.getTop(); 1131 final float max = trackImage.getBottom(); 1132 final float offset = min; 1133 final float range = max - min; 1134 final float thumbMiddle = position * range + offset; 1135 thumbImage.setTranslationY(thumbMiddle - thumbImage.getHeight() / 2); 1136 1137 final float previewPos = mOverlayPosition == OVERLAY_AT_THUMB ? thumbMiddle : 0; 1138 1139 // Center the preview on the thumb, constrained to the list bounds. 1140 final View previewImage = mPreviewImage; 1141 final float previewHalfHeight = previewImage.getHeight() / 2f; 1142 final float minP = top + previewHalfHeight; 1143 final float maxP = bottom - previewHalfHeight; 1144 final float previewMiddle = MathUtils.constrain(previewPos, minP, maxP); 1145 final float previewTop = previewMiddle - previewHalfHeight; 1146 previewImage.setTranslationY(previewTop); 1147 1148 mPrimaryText.setTranslationY(previewTop); 1149 mSecondaryText.setTranslationY(previewTop); 1150 } 1151 1152 private float getPosFromMotionEvent(float y) { 1153 final View trackImage = mTrackImage; 1154 final float min = trackImage.getTop(); 1155 final float max = trackImage.getBottom(); 1156 final float offset = min; 1157 final float range = max - min; 1158 1159 // If the list is the same height as the thumbnail or shorter, 1160 // effectively disable scrolling. 1161 if (range <= 0) { 1162 return 0f; 1163 } 1164 1165 return MathUtils.constrain((y - offset) / range, 0f, 1f); 1166 } 1167 1168 private float getPosFromItemCount( 1169 int firstVisibleItem, int visibleItemCount, int totalItemCount) { 1170 if (mSectionIndexer == null || mListAdapter == null) { 1171 getSectionsFromIndexer(); 1172 } 1173 1174 final boolean hasSections = mSectionIndexer != null && mSections != null 1175 && mSections.length > 0; 1176 if (!hasSections || !mMatchDragPosition) { 1177 return (float) firstVisibleItem / (totalItemCount - visibleItemCount); 1178 } 1179 1180 // Ignore headers. 1181 firstVisibleItem -= mHeaderCount; 1182 if (firstVisibleItem < 0) { 1183 return 0; 1184 } 1185 totalItemCount -= mHeaderCount; 1186 1187 // Hidden portion of the first visible row. 1188 final View child = mList.getChildAt(0); 1189 final float incrementalPos; 1190 if (child == null || child.getHeight() == 0) { 1191 incrementalPos = 0; 1192 } else { 1193 incrementalPos = (float) (mList.getPaddingTop() - child.getTop()) / child.getHeight(); 1194 } 1195 1196 // Number of rows in this section. 1197 final int section = mSectionIndexer.getSectionForPosition(firstVisibleItem); 1198 final int sectionPos = mSectionIndexer.getPositionForSection(section); 1199 final int sectionCount = mSections.length; 1200 final int positionsInSection; 1201 if (section < sectionCount - 1) { 1202 final int nextSectionPos; 1203 if (section + 1 < sectionCount) { 1204 nextSectionPos = mSectionIndexer.getPositionForSection(section + 1); 1205 } else { 1206 nextSectionPos = totalItemCount - 1; 1207 } 1208 positionsInSection = nextSectionPos - sectionPos; 1209 } else { 1210 positionsInSection = totalItemCount - sectionPos; 1211 } 1212 1213 // Position within this section. 1214 final float posWithinSection; 1215 if (positionsInSection == 0) { 1216 posWithinSection = 0; 1217 } else { 1218 posWithinSection = (firstVisibleItem + incrementalPos - sectionPos) 1219 / positionsInSection; 1220 } 1221 1222 float result = (section + posWithinSection) / sectionCount; 1223 1224 // Fake out the scroll bar for the last item. Since the section indexer 1225 // won't ever actually move the list in this end space, make scrolling 1226 // across the last item account for whatever space is remaining. 1227 if (firstVisibleItem > 0 && firstVisibleItem + visibleItemCount == totalItemCount) { 1228 final View lastChild = mList.getChildAt(visibleItemCount - 1); 1229 final float lastItemVisible = (float) (mList.getHeight() - mList.getPaddingBottom() 1230 - lastChild.getTop()) / lastChild.getHeight(); 1231 result += (1 - result) * lastItemVisible; 1232 } 1233 1234 return result; 1235 } 1236 1237 /** 1238 * Cancels an ongoing fling event by injecting a 1239 * {@link MotionEvent#ACTION_CANCEL} into the host view. 1240 */ 1241 private void cancelFling() { 1242 final MotionEvent cancelFling = MotionEvent.obtain( 1243 0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0); 1244 mList.onTouchEvent(cancelFling); 1245 cancelFling.recycle(); 1246 } 1247 1248 /** 1249 * Cancels a pending drag. 1250 * 1251 * @see #startPendingDrag() 1252 */ 1253 private void cancelPendingDrag() { 1254 mPendingDrag = -1; 1255 } 1256 1257 /** 1258 * Delays dragging until after the framework has determined that the user is 1259 * scrolling, rather than tapping. 1260 */ 1261 private void startPendingDrag() { 1262 mPendingDrag = SystemClock.uptimeMillis() + TAP_TIMEOUT; 1263 } 1264 1265 private void beginDrag() { 1266 mPendingDrag = -1; 1267 1268 setState(STATE_DRAGGING); 1269 1270 if (mListAdapter == null && mList != null) { 1271 getSectionsFromIndexer(); 1272 } 1273 1274 if (mList != null) { 1275 mList.requestDisallowInterceptTouchEvent(true); 1276 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 1277 } 1278 1279 cancelFling(); 1280 } 1281 1282 public boolean onInterceptTouchEvent(MotionEvent ev) { 1283 if (!isEnabled()) { 1284 return false; 1285 } 1286 1287 switch (ev.getActionMasked()) { 1288 case MotionEvent.ACTION_DOWN: 1289 if (isPointInside(ev.getX(), ev.getY())) { 1290 // If the parent has requested that its children delay 1291 // pressed state (e.g. is a scrolling container) then we 1292 // need to allow the parent time to decide whether it wants 1293 // to intercept events. If it does, we will receive a CANCEL 1294 // event. 1295 if (!mList.isInScrollingContainer()) { 1296 beginDrag(); 1297 return true; 1298 } 1299 1300 mInitialTouchY = ev.getY(); 1301 startPendingDrag(); 1302 } 1303 break; 1304 case MotionEvent.ACTION_MOVE: 1305 if (!isPointInside(ev.getX(), ev.getY())) { 1306 cancelPendingDrag(); 1307 } else if (mPendingDrag >= 0 && mPendingDrag <= SystemClock.uptimeMillis()) { 1308 beginDrag(); 1309 1310 final float pos = getPosFromMotionEvent(mInitialTouchY); 1311 scrollTo(pos); 1312 1313 return onTouchEvent(ev); 1314 } 1315 break; 1316 case MotionEvent.ACTION_UP: 1317 case MotionEvent.ACTION_CANCEL: 1318 cancelPendingDrag(); 1319 break; 1320 } 1321 1322 return false; 1323 } 1324 1325 public boolean onInterceptHoverEvent(MotionEvent ev) { 1326 if (!isEnabled()) { 1327 return false; 1328 } 1329 1330 final int actionMasked = ev.getActionMasked(); 1331 if ((actionMasked == MotionEvent.ACTION_HOVER_ENTER 1332 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) && mState == STATE_NONE 1333 && isPointInside(ev.getX(), ev.getY())) { 1334 setState(STATE_VISIBLE); 1335 postAutoHide(); 1336 } 1337 1338 return false; 1339 } 1340 1341 public boolean onTouchEvent(MotionEvent me) { 1342 if (!isEnabled()) { 1343 return false; 1344 } 1345 1346 switch (me.getActionMasked()) { 1347 case MotionEvent.ACTION_UP: { 1348 if (mPendingDrag >= 0) { 1349 // Allow a tap to scroll. 1350 beginDrag(); 1351 1352 final float pos = getPosFromMotionEvent(me.getY()); 1353 setThumbPos(pos); 1354 scrollTo(pos); 1355 1356 // Will hit the STATE_DRAGGING check below 1357 } 1358 1359 if (mState == STATE_DRAGGING) { 1360 if (mList != null) { 1361 // ViewGroup does the right thing already, but there might 1362 // be other classes that don't properly reset on touch-up, 1363 // so do this explicitly just in case. 1364 mList.requestDisallowInterceptTouchEvent(false); 1365 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 1366 } 1367 1368 setState(STATE_VISIBLE); 1369 postAutoHide(); 1370 1371 return true; 1372 } 1373 } break; 1374 1375 case MotionEvent.ACTION_MOVE: { 1376 if (mPendingDrag >= 0 && Math.abs(me.getY() - mInitialTouchY) > mScaledTouchSlop) { 1377 beginDrag(); 1378 1379 // Will hit the STATE_DRAGGING check below 1380 } 1381 1382 if (mState == STATE_DRAGGING) { 1383 // TODO: Ignore jitter. 1384 final float pos = getPosFromMotionEvent(me.getY()); 1385 setThumbPos(pos); 1386 1387 // If the previous scrollTo is still pending 1388 if (mScrollCompleted) { 1389 scrollTo(pos); 1390 } 1391 1392 return true; 1393 } 1394 } break; 1395 1396 case MotionEvent.ACTION_CANCEL: { 1397 cancelPendingDrag(); 1398 } break; 1399 } 1400 1401 return false; 1402 } 1403 1404 /** 1405 * Returns whether a coordinate is inside the scroller's activation area. If 1406 * there is a track image, touching anywhere within the thumb-width of the 1407 * track activates scrolling. Otherwise, the user has to touch inside thumb 1408 * itself. 1409 * 1410 * @param x The x-coordinate. 1411 * @param y The y-coordinate. 1412 * @return Whether the coordinate is inside the scroller's activation area. 1413 */ 1414 private boolean isPointInside(float x, float y) { 1415 return isPointInsideX(x) && (mTrackDrawable != null || isPointInsideY(y)); 1416 } 1417 1418 private boolean isPointInsideX(float x) { 1419 if (mLayoutFromRight) { 1420 return x >= mThumbImage.getLeft(); 1421 } else { 1422 return x <= mThumbImage.getRight(); 1423 } 1424 } 1425 1426 private boolean isPointInsideY(float y) { 1427 final float offset = mThumbImage.getTranslationY(); 1428 final float top = mThumbImage.getTop() + offset; 1429 final float bottom = mThumbImage.getBottom() + offset; 1430 return y >= top && y <= bottom; 1431 } 1432 1433 /** 1434 * Constructs an animator for the specified property on a group of views. 1435 * See {@link ObjectAnimator#ofFloat(Object, String, float...)} for 1436 * implementation details. 1437 * 1438 * @param property The property being animated. 1439 * @param value The value to which that property should animate. 1440 * @param views The target views to animate. 1441 * @return An animator for all the specified views. 1442 */ 1443 private static Animator groupAnimatorOfFloat( 1444 Property<View, Float> property, float value, View... views) { 1445 AnimatorSet animSet = new AnimatorSet(); 1446 AnimatorSet.Builder builder = null; 1447 1448 for (int i = views.length - 1; i >= 0; i--) { 1449 final Animator anim = ObjectAnimator.ofFloat(views[i], property, value); 1450 if (builder == null) { 1451 builder = animSet.play(anim); 1452 } else { 1453 builder.with(anim); 1454 } 1455 } 1456 1457 return animSet; 1458 } 1459 1460 /** 1461 * Returns an animator for the view's scaleX value. 1462 */ 1463 private static Animator animateScaleX(View v, float target) { 1464 return ObjectAnimator.ofFloat(v, View.SCALE_X, target); 1465 } 1466 1467 /** 1468 * Returns an animator for the view's alpha value. 1469 */ 1470 private static Animator animateAlpha(View v, float alpha) { 1471 return ObjectAnimator.ofFloat(v, View.ALPHA, alpha); 1472 } 1473 1474 /** 1475 * A Property wrapper around the <code>left</code> functionality handled by the 1476 * {@link View#setLeft(int)} and {@link View#getLeft()} methods. 1477 */ 1478 private static Property<View, Integer> LEFT = new IntProperty<View>("left") { 1479 @Override 1480 public void setValue(View object, int value) { 1481 object.setLeft(value); 1482 } 1483 1484 @Override 1485 public Integer get(View object) { 1486 return object.getLeft(); 1487 } 1488 }; 1489 1490 /** 1491 * A Property wrapper around the <code>top</code> functionality handled by the 1492 * {@link View#setTop(int)} and {@link View#getTop()} methods. 1493 */ 1494 private static Property<View, Integer> TOP = new IntProperty<View>("top") { 1495 @Override 1496 public void setValue(View object, int value) { 1497 object.setTop(value); 1498 } 1499 1500 @Override 1501 public Integer get(View object) { 1502 return object.getTop(); 1503 } 1504 }; 1505 1506 /** 1507 * A Property wrapper around the <code>right</code> functionality handled by the 1508 * {@link View#setRight(int)} and {@link View#getRight()} methods. 1509 */ 1510 private static Property<View, Integer> RIGHT = new IntProperty<View>("right") { 1511 @Override 1512 public void setValue(View object, int value) { 1513 object.setRight(value); 1514 } 1515 1516 @Override 1517 public Integer get(View object) { 1518 return object.getRight(); 1519 } 1520 }; 1521 1522 /** 1523 * A Property wrapper around the <code>bottom</code> functionality handled by the 1524 * {@link View#setBottom(int)} and {@link View#getBottom()} methods. 1525 */ 1526 private static Property<View, Integer> BOTTOM = new IntProperty<View>("bottom") { 1527 @Override 1528 public void setValue(View object, int value) { 1529 object.setBottom(value); 1530 } 1531 1532 @Override 1533 public Integer get(View object) { 1534 return object.getBottom(); 1535 } 1536 }; 1537 1538 /** 1539 * Returns an animator for the view's bounds. 1540 */ 1541 private static Animator animateBounds(View v, Rect bounds) { 1542 final PropertyValuesHolder left = PropertyValuesHolder.ofInt(LEFT, bounds.left); 1543 final PropertyValuesHolder top = PropertyValuesHolder.ofInt(TOP, bounds.top); 1544 final PropertyValuesHolder right = PropertyValuesHolder.ofInt(RIGHT, bounds.right); 1545 final PropertyValuesHolder bottom = PropertyValuesHolder.ofInt(BOTTOM, bounds.bottom); 1546 return ObjectAnimator.ofPropertyValuesHolder(v, left, top, right, bottom); 1547 } 1548} 1549