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