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