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