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