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