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