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