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