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