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