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