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