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