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