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