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