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