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