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