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