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