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