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