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