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