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