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