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