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