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