blob: 4b5407a8a5dcba598925709ec13c4198ce1c9586 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
17package android.widget;
18
Alan Viverette0ebe81e2013-06-21 17:01:36 -070019import android.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.animation.PropertyValuesHolder;
Tor Norbye417ee5b2015-03-10 20:57:37 -070025import android.annotation.StyleRes;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026import android.content.Context;
27import android.content.res.ColorStateList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028import android.content.res.TypedArray;
Adam Powell20232d02010-12-08 21:08:53 -080029import android.graphics.Rect;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030import android.graphics.drawable.Drawable;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070031import android.os.Build;
Alan Viverette4cfeedb2013-12-03 14:03:24 -080032import android.os.SystemClock;
Alan Viverettef9af7b22013-10-15 17:09:35 -070033import android.text.TextUtils;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070034import android.text.TextUtils.TruncateAt;
35import android.util.IntProperty;
36import android.util.MathUtils;
37import android.util.Property;
Alan Viverette7b630632013-10-04 11:44:51 -070038import android.util.TypedValue;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070039import android.view.Gravity;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040import android.view.MotionEvent;
Adam Powell20232d02010-12-08 21:08:53 -080041import android.view.View;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070042import android.view.View.MeasureSpec;
Adam Powellaf5280c2011-10-11 18:36:34 -070043import android.view.ViewConfiguration;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070044import android.view.ViewGroup.LayoutParams;
45import android.view.ViewGroupOverlay;
Adam Powelld43bd482010-02-26 16:29:09 -080046import android.widget.AbsListView.OnScrollListener;
Alan Viverette8636ace2013-10-31 15:41:31 -070047import android.widget.ImageView.ScaleType;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070048
Alan Viverettee5f1fff2015-04-07 14:39:04 -070049import com.android.internal.R;
50
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051/**
52 * Helper class for AbsListView to draw and control the Fast Scroll thumb
53 */
54class FastScroller {
Alan Viverette0ebe81e2013-06-21 17:01:36 -070055 /** Duration of fade-out animation. */
56 private static final int DURATION_FADE_OUT = 300;
Alan Viverettee918a482013-06-07 11:43:06 -070057
Alan Viverette0ebe81e2013-06-21 17:01:36 -070058 /** 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. */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080074 private static final int STATE_NONE = 0;
Adam Powell20232d02010-12-08 21:08:53 -080075
Alan Viverette0ebe81e2013-06-21 17:01:36 -070076 /** Scroll thumb visible and moving along with the scrollbar. */
77 private static final int STATE_VISIBLE = 1;
Adam Powell20232d02010-12-08 21:08:53 -080078
Alan Viverette0ebe81e2013-06-21 17:01:36 -070079 /** Scroll thumb and preview being dragged by user. */
80 private static final int STATE_DRAGGING = 2;
Adam Powell20232d02010-12-08 21:08:53 -080081
Alan Viverette0ebe81e2013-06-21 17:01:36 -070082 // Positions for preview image and text.
Adam Powell20232d02010-12-08 21:08:53 -080083 private static final int OVERLAY_FLOATING = 0;
84 private static final int OVERLAY_AT_THUMB = 1;
Alan Viverettee8311ac2014-08-15 19:58:04 -070085 private static final int OVERLAY_ABOVE_THUMB = 2;
Alan Viverettee918a482013-06-07 11:43:06 -070086
Alan Viverettee5f1fff2015-04-07 14:39:04 -070087 // 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
Alan Viverette0ebe81e2013-06-21 17:01:36 -070091 // Indices for mPreviewResId.
92 private static final int PREVIEW_LEFT = 0;
93 private static final int PREVIEW_RIGHT = 1;
Adam Powell20232d02010-12-08 21:08:53 -080094
Alan Viverette0ebe81e2013-06-21 17:01:36 -070095 /** Delay before considering a tap in the thumb area to be a drag. */
96 private static final long TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080097
Alan Viverette0ebe81e2013-06-21 17:01:36 -070098 private final Rect mTempBounds = new Rect();
99 private final Rect mTempMargins = new Rect();
Alan Viverettefb664152013-08-07 17:57:51 -0700100 private final Rect mContainerRect = new Rect();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800101
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700102 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;
Alan Viverette8636ace2013-10-31 15:41:31 -0700108 private final View mPreviewImage;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700109 /**
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
Alan Viverette7d5bcd72014-11-20 16:07:59 -0800115 /** The minimum touch target size in pixels. */
116 private final int mMinimumTouchTarget;
117
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700118 /**
119 * Padding in pixels around the preview text. Applied as layout margins to
120 * the preview text and padding to the preview image.
121 */
Alan Viverette8636ace2013-10-31 15:41:31 -0700122 private int mPreviewPadding;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700123
Alan Viverette8636ace2013-10-31 15:41:31 -0700124 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;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700138 private int mThumbPosition;
139
140 // Used to convert between y-coordinate and thumb position within track.
141 private float mThumbOffset;
142 private float mThumbRange;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700143
Alan Viverette26bb2532013-08-09 10:40:50 -0700144 /** Total width of decorations. */
Alan Viverette8636ace2013-10-31 15:41:31 -0700145 private int mWidth;
Alan Viverette26bb2532013-08-09 10:40:50 -0700146
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700147 /** 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
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700165 /** The index of the current section. */
166 private int mCurrentSection = -1;
167
Alan Viverette26bb2532013-08-09 10:40:50 -0700168 /** The current scrollbar position. */
169 private int mScrollbarPosition = -1;
170
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700171 /** Whether the list is long enough to need a fast scroller. */
The Android Open Source Project4df24232009-03-05 14:34:35 -0800172 private boolean mLongList;
Alan Viverettee918a482013-06-07 11:43:06 -0700173
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700174 private Object[] mSections;
Alan Viverettee918a482013-06-07 11:43:06 -0700175
Alan Viverettefb664152013-08-07 17:57:51 -0700176 /** Whether this view is currently performing layout. */
177 private boolean mUpdatingLayout;
178
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700179 /**
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 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800187 private int mState;
Alan Viverettee918a482013-06-07 11:43:06 -0700188
Alan Viverettef9af7b22013-10-15 17:09:35 -0700189 /** Whether the preview image is visible. */
190 private boolean mShowingPreview;
191
Alan Viverette4f434c72013-12-13 11:01:21 -0800192 private Adapter mListAdapter;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800193 private SectionIndexer mSectionIndexer;
194
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700195 /** Whether decorations should be laid out from right to left. */
196 private boolean mLayoutFromRight;
Alan Viverettee918a482013-06-07 11:43:06 -0700197
Alan Viverette447cdf22013-07-15 17:47:34 -0700198 /** Whether the fast scroller is enabled. */
199 private boolean mEnabled;
200
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700201 /** Whether the scrollbar and decorations should always be shown. */
Adam Powell20232d02010-12-08 21:08:53 -0800202 private boolean mAlwaysShow;
203
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700204 /**
205 * Position for the preview image and text. One of:
206 * <ul>
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700207 * <li>{@link #OVERLAY_FLOATING}
Alan Viverettee8311ac2014-08-15 19:58:04 -0700208 * <li>{@link #OVERLAY_AT_THUMB}
209 * <li>{@link #OVERLAY_ABOVE_THUMB}
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700210 * </ul>
211 */
Adam Powell20232d02010-12-08 21:08:53 -0800212 private int mOverlayPosition;
213
Alan Viverette26bb2532013-08-09 10:40:50 -0700214 /** Current scrollbar style, including inset and overlay properties. */
215 private int mScrollBarStyle;
216
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700217 /** Whether to precisely match the thumb position to the list. */
Adam Powell568ccd82011-08-03 22:38:48 -0700218 private boolean mMatchDragPosition;
219
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700220 private float mInitialTouchY;
Alan Viverette4cfeedb2013-12-03 14:03:24 -0800221 private long mPendingDrag = -1;
Adam Powellaf5280c2011-10-11 18:36:34 -0700222 private int mScaledTouchSlop;
223
Alan Viverette4b95cc72014-01-14 16:54:02 -0800224 private int mOldItemCount;
225 private int mOldChildCount;
226
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700227 /**
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;
Adam Powellaf5280c2011-10-11 18:36:34 -0700244 }
245 };
246
Alan Viverette8636ace2013-10-31 15:41:31 -0700247 public FastScroller(AbsListView listView, int styleResId) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800248 mList = listView;
Alan Viverette4b95cc72014-01-14 16:54:02 -0800249 mOldItemCount = listView.getCount();
250 mOldChildCount = listView.getChildCount();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700251
Alan Viverette39bed692013-08-07 15:47:04 -0700252 final Context context = listView.getContext();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700253 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
Alan Viverette8636ace2013-10-31 15:41:31 -0700254 mScrollBarStyle = listView.getScrollBarStyle();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700255
Alan Viverette8636ace2013-10-31 15:41:31 -0700256 mScrollCompleted = true;
257 mState = STATE_VISIBLE;
258 mMatchDragPosition =
259 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700260
Alan Viverette8636ace2013-10-31 15:41:31 -0700261 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);
Alan Viverette26bb2532013-08-09 10:40:50 -0700267
Alan Viverette8636ace2013-10-31 15:41:31 -0700268 mPrimaryText = createPreviewTextView(context);
269 mSecondaryText = createPreviewTextView(context);
270
Alan Viverette7d5bcd72014-11-20 16:07:59 -0800271 mMinimumTouchTarget = listView.getResources().getDimensionPixelSize(
272 com.android.internal.R.dimen.fast_scroller_minimum_touch_target);
273
Alan Viverette8636ace2013-10-31 15:41:31 -0700274 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();
Alan Viverette4b95cc72014-01-14 16:54:02 -0800285 updateLongList(mOldChildCount, mOldItemCount);
Alan Viverette8636ace2013-10-31 15:41:31 -0700286 setScrollbarPosition(listView.getVerticalScrollbarPosition());
287 postAutoHide();
288 }
289
290 private void updateAppearance() {
Alan Viverette26bb2532013-08-09 10:40:50 -0700291 int width = 0;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700292
293 // Add track to overlay if it has an image.
Alan Viverette8636ace2013-10-31 15:41:31 -0700294 mTrackImage.setImageDrawable(mTrackDrawable);
295 if (mTrackDrawable != null) {
296 width = Math.max(width, mTrackDrawable.getIntrinsicWidth());
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700297 }
298
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700299 // Add thumb to overlay if it has an image.
Alan Viverette8636ace2013-10-31 15:41:31 -0700300 mThumbImage.setImageDrawable(mThumbDrawable);
301 mThumbImage.setMinimumWidth(mThumbMinWidth);
302 mThumbImage.setMinimumHeight(mThumbMinHeight);
303 if (mThumbDrawable != null) {
304 width = Math.max(width, mThumbDrawable.getIntrinsicWidth());
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700305 }
306
Alan Viverette8636ace2013-10-31 15:41:31 -0700307 // Account for minimum thumb width.
308 mWidth = Math.max(width, mThumbMinWidth);
309
Alan Viverette8636ace2013-10-31 15:41:31 -0700310 if (mTextAppearance != 0) {
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700311 mPrimaryText.setTextAppearance(mTextAppearance);
312 mSecondaryText.setTextAppearance(mTextAppearance);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700313 }
314
Alan Viverette8636ace2013-10-31 15:41:31 -0700315 if (mTextColor != null) {
316 mPrimaryText.setTextColor(mTextColor);
317 mSecondaryText.setTextColor(mTextColor);
318 }
Alan Viverette26bb2532013-08-09 10:40:50 -0700319
Alan Viverette8636ace2013-10-31 15:41:31 -0700320 if (mTextSize > 0) {
321 mPrimaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
322 mSecondaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
323 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700324
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700325 final int padding = mPreviewPadding;
Alan Viverettee8311ac2014-08-15 19:58:04 -0700326 mPrimaryText.setIncludeFontPadding(false);
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700327 mPrimaryText.setPadding(padding, padding, padding, padding);
Alan Viverettee8311ac2014-08-15 19:58:04 -0700328 mSecondaryText.setIncludeFontPadding(false);
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700329 mSecondaryText.setPadding(padding, padding, padding, padding);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700330
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700331 refreshDrawablePressedState();
Alan Viverette8636ace2013-10-31 15:41:31 -0700332 }
333
Tor Norbye417ee5b2015-03-10 20:57:37 -0700334 public void setStyle(@StyleRes int resId) {
Alan Viverette8636ace2013-10-31 15:41:31 -0700335 final Context context = mList.getContext();
336 final TypedArray ta = context.obtainStyledAttributes(null,
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700337 R.styleable.FastScroll, R.attr.fastScrollStyle, resId);
Alan Viverette8636ace2013-10-31 15:41:31 -0700338 final int N = ta.getIndexCount();
339 for (int i = 0; i < N; i++) {
340 final int index = ta.getIndex(i);
341 switch (index) {
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700342 case R.styleable.FastScroll_position:
Alan Viverette8636ace2013-10-31 15:41:31 -0700343 mOverlayPosition = ta.getInt(index, OVERLAY_FLOATING);
344 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700345 case R.styleable.FastScroll_backgroundLeft:
Alan Viverette8636ace2013-10-31 15:41:31 -0700346 mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(index, 0);
347 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700348 case R.styleable.FastScroll_backgroundRight:
Alan Viverette8636ace2013-10-31 15:41:31 -0700349 mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(index, 0);
350 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700351 case R.styleable.FastScroll_thumbDrawable:
Alan Viverette8636ace2013-10-31 15:41:31 -0700352 mThumbDrawable = ta.getDrawable(index);
353 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700354 case R.styleable.FastScroll_trackDrawable:
Alan Viverette8636ace2013-10-31 15:41:31 -0700355 mTrackDrawable = ta.getDrawable(index);
356 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700357 case R.styleable.FastScroll_textAppearance:
Alan Viverette8636ace2013-10-31 15:41:31 -0700358 mTextAppearance = ta.getResourceId(index, 0);
359 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700360 case R.styleable.FastScroll_textColor:
Alan Viverette8636ace2013-10-31 15:41:31 -0700361 mTextColor = ta.getColorStateList(index);
362 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700363 case R.styleable.FastScroll_textSize:
Alan Viverette8636ace2013-10-31 15:41:31 -0700364 mTextSize = ta.getDimensionPixelSize(index, 0);
365 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700366 case R.styleable.FastScroll_minWidth:
Alan Viverette8636ace2013-10-31 15:41:31 -0700367 mPreviewMinWidth = ta.getDimensionPixelSize(index, 0);
368 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700369 case R.styleable.FastScroll_minHeight:
Alan Viverette8636ace2013-10-31 15:41:31 -0700370 mPreviewMinHeight = ta.getDimensionPixelSize(index, 0);
371 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700372 case R.styleable.FastScroll_thumbMinWidth:
Alan Viverette8636ace2013-10-31 15:41:31 -0700373 mThumbMinWidth = ta.getDimensionPixelSize(index, 0);
374 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700375 case R.styleable.FastScroll_thumbMinHeight:
Alan Viverette8636ace2013-10-31 15:41:31 -0700376 mThumbMinHeight = ta.getDimensionPixelSize(index, 0);
377 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700378 case R.styleable.FastScroll_padding:
Alan Viverette8636ace2013-10-31 15:41:31 -0700379 mPreviewPadding = ta.getDimensionPixelSize(index, 0);
380 break;
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700381 case R.styleable.FastScroll_thumbPosition:
382 mThumbPosition = ta.getInt(index, THUMB_POSITION_MIDPOINT);
383 break;
Alan Viverette8636ace2013-10-31 15:41:31 -0700384 }
385 }
386
387 updateAppearance();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800388 }
389
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700390 /**
Alan Viverette447cdf22013-07-15 17:47:34 -0700391 * Removes this FastScroller overlay from the host view.
392 */
393 public void remove() {
394 mOverlay.remove(mTrackImage);
395 mOverlay.remove(mThumbImage);
396 mOverlay.remove(mPreviewImage);
397 mOverlay.remove(mPrimaryText);
398 mOverlay.remove(mSecondaryText);
399 }
400
401 /**
402 * @param enabled Whether the fast scroll thumb is enabled.
403 */
404 public void setEnabled(boolean enabled) {
Alan Viveretteb9f27222013-09-06 19:39:47 -0700405 if (mEnabled != enabled) {
406 mEnabled = enabled;
Alan Viverette447cdf22013-07-15 17:47:34 -0700407
Alan Viverette8ac22b02013-12-12 18:35:40 -0800408 onStateDependencyChanged(true);
Alan Viverette447cdf22013-07-15 17:47:34 -0700409 }
410 }
411
412 /**
413 * @return Whether the fast scroll thumb is enabled.
414 */
415 public boolean isEnabled() {
Alan Viveretteb9f27222013-09-06 19:39:47 -0700416 return mEnabled && (mLongList || mAlwaysShow);
Alan Viverette447cdf22013-07-15 17:47:34 -0700417 }
418
419 /**
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700420 * @param alwaysShow Whether the fast scroll thumb should always be shown
421 */
Adam Powell20232d02010-12-08 21:08:53 -0800422 public void setAlwaysShow(boolean alwaysShow) {
Alan Viveretteb9f27222013-09-06 19:39:47 -0700423 if (mAlwaysShow != alwaysShow) {
424 mAlwaysShow = alwaysShow;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700425
Alan Viverette8ac22b02013-12-12 18:35:40 -0800426 onStateDependencyChanged(false);
Adam Powell20232d02010-12-08 21:08:53 -0800427 }
428 }
429
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700430 /**
431 * @return Whether the fast scroll thumb will always be shown
432 * @see #setAlwaysShow(boolean)
433 */
Adam Powell20232d02010-12-08 21:08:53 -0800434 public boolean isAlwaysShowEnabled() {
435 return mAlwaysShow;
436 }
437
Alan Viveretteb9f27222013-09-06 19:39:47 -0700438 /**
439 * Called when one of the variables affecting enabled state changes.
Alan Viverette8ac22b02013-12-12 18:35:40 -0800440 *
441 * @param peekIfEnabled whether the thumb should peek, if enabled
Alan Viveretteb9f27222013-09-06 19:39:47 -0700442 */
Alan Viverette8ac22b02013-12-12 18:35:40 -0800443 private void onStateDependencyChanged(boolean peekIfEnabled) {
Alan Viveretteb9f27222013-09-06 19:39:47 -0700444 if (isEnabled()) {
445 if (isAlwaysShowEnabled()) {
446 setState(STATE_VISIBLE);
447 } else if (mState == STATE_VISIBLE) {
448 postAutoHide();
Alan Viverette8ac22b02013-12-12 18:35:40 -0800449 } else if (peekIfEnabled) {
450 setState(STATE_VISIBLE);
451 postAutoHide();
Alan Viveretteb9f27222013-09-06 19:39:47 -0700452 }
453 } else {
454 stop();
455 }
456
457 mList.resolvePadding();
458 }
459
Alan Viverette26bb2532013-08-09 10:40:50 -0700460 public void setScrollBarStyle(int style) {
461 if (mScrollBarStyle != style) {
462 mScrollBarStyle = style;
463
464 updateLayout();
465 }
466 }
467
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700468 /**
469 * Immediately transitions the fast scroller decorations to a hidden state.
470 */
471 public void stop() {
472 setState(STATE_NONE);
473 }
Adam Powell20232d02010-12-08 21:08:53 -0800474
Adam Powell20232d02010-12-08 21:08:53 -0800475 public void setScrollbarPosition(int position) {
Fabrice Di Meglioc23ee462012-06-22 18:46:06 -0700476 if (position == View.SCROLLBAR_POSITION_DEFAULT) {
477 position = mList.isLayoutRtl() ?
478 View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT;
479 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700480
Alan Viverette26bb2532013-08-09 10:40:50 -0700481 if (mScrollbarPosition != position) {
482 mScrollbarPosition = position;
483 mLayoutFromRight = position != View.SCROLLBAR_POSITION_LEFT;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700484
Alan Viverette26bb2532013-08-09 10:40:50 -0700485 final int previewResId = mPreviewResId[mLayoutFromRight ? PREVIEW_RIGHT : PREVIEW_LEFT];
486 mPreviewImage.setBackgroundResource(previewResId);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700487
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700488 // Propagate padding to text min width/height.
489 final int textMinWidth = Math.max(0, mPreviewMinWidth - mPreviewImage.getPaddingLeft()
490 - mPreviewImage.getPaddingRight());
491 mPrimaryText.setMinimumWidth(textMinWidth);
492 mSecondaryText.setMinimumWidth(textMinWidth);
493
494 final int textMinHeight = Math.max(0, mPreviewMinHeight - mPreviewImage.getPaddingTop()
495 - mPreviewImage.getPaddingBottom());
496 mPrimaryText.setMinimumHeight(textMinHeight);
497 mSecondaryText.setMinimumHeight(textMinHeight);
Alan Viverette26bb2532013-08-09 10:40:50 -0700498
499 // Requires re-layout.
500 updateLayout();
Adam Powell20232d02010-12-08 21:08:53 -0800501 }
502 }
503
504 public int getWidth() {
Alan Viverette26bb2532013-08-09 10:40:50 -0700505 return mWidth;
Adam Powell20232d02010-12-08 21:08:53 -0800506 }
507
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700508 public void onSizeChanged(int w, int h, int oldw, int oldh) {
509 updateLayout();
510 }
511
Alan Viverette4b95cc72014-01-14 16:54:02 -0800512 public void onItemCountChanged(int childCount, int itemCount) {
513 if (mOldItemCount != itemCount || mOldChildCount != childCount) {
514 mOldItemCount = itemCount;
515 mOldChildCount = childCount;
Alan Viveretteb9f27222013-09-06 19:39:47 -0700516
Alan Viverette4b95cc72014-01-14 16:54:02 -0800517 final boolean hasMoreItems = itemCount - childCount > 0;
518 if (hasMoreItems && mState != STATE_DRAGGING) {
519 final int firstVisibleItem = mList.getFirstVisiblePosition();
520 setThumbPos(getPosFromItemCount(firstVisibleItem, childCount, itemCount));
521 }
522
523 updateLongList(childCount, itemCount);
524 }
Alan Viveretteb9f27222013-09-06 19:39:47 -0700525 }
526
Alan Viverette4b95cc72014-01-14 16:54:02 -0800527 private void updateLongList(int childCount, int itemCount) {
528 final boolean longList = childCount > 0 && itemCount / childCount >= MIN_PAGES;
Alan Viveretteb9f27222013-09-06 19:39:47 -0700529 if (mLongList != longList) {
530 mLongList = longList;
531
Alan Viverette8ac22b02013-12-12 18:35:40 -0800532 onStateDependencyChanged(false);
Alan Viveretteb9f27222013-09-06 19:39:47 -0700533 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800534 }
Alan Viverettee918a482013-06-07 11:43:06 -0700535
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700536 /**
537 * Creates a view into which preview text can be placed.
538 */
Alan Viverette8636ace2013-10-31 15:41:31 -0700539 private TextView createPreviewTextView(Context context) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700540 final LayoutParams params = new LayoutParams(
541 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700542 final TextView textView = new TextView(context);
543 textView.setLayoutParams(params);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700544 textView.setSingleLine(true);
545 textView.setEllipsize(TruncateAt.MIDDLE);
546 textView.setGravity(Gravity.CENTER);
547 textView.setAlpha(0f);
Adam Powell20232d02010-12-08 21:08:53 -0800548
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700549 // Manually propagate inherited layout direction.
550 textView.setLayoutDirection(mList.getLayoutDirection());
NoraBora9b38c602010-10-12 06:59:55 -0700551
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700552 return textView;
553 }
554
555 /**
556 * Measures and layouts the scrollbar and decorations.
557 */
Alan Viverette26bb2532013-08-09 10:40:50 -0700558 public void updateLayout() {
Alan Viverettefb664152013-08-07 17:57:51 -0700559 // Prevent re-entry when RTL properties change as a side-effect of
560 // resolving padding.
561 if (mUpdatingLayout) {
562 return;
563 }
564
565 mUpdatingLayout = true;
566
567 updateContainerRect();
568
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700569 layoutThumb();
570 layoutTrack();
571
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700572 updateOffsetAndRange();
573
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700574 final Rect bounds = mTempBounds;
575 measurePreview(mPrimaryText, bounds);
576 applyLayout(mPrimaryText, bounds);
577 measurePreview(mSecondaryText, bounds);
578 applyLayout(mSecondaryText, bounds);
579
580 if (mPreviewImage != null) {
581 // Apply preview image padding.
582 bounds.left -= mPreviewImage.getPaddingLeft();
583 bounds.top -= mPreviewImage.getPaddingTop();
584 bounds.right += mPreviewImage.getPaddingRight();
585 bounds.bottom += mPreviewImage.getPaddingBottom();
586 applyLayout(mPreviewImage, bounds);
587 }
Alan Viverettefb664152013-08-07 17:57:51 -0700588
589 mUpdatingLayout = false;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700590 }
591
592 /**
593 * Layouts a view within the specified bounds and pins the pivot point to
594 * the appropriate edge.
595 *
596 * @param view The view to layout.
597 * @param bounds Bounds at which to layout the view.
598 */
599 private void applyLayout(View view, Rect bounds) {
600 view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom);
601 view.setPivotX(mLayoutFromRight ? bounds.right - bounds.left : 0);
602 }
603
604 /**
605 * Measures the preview text bounds, taking preview image padding into
606 * account. This method should only be called after {@link #layoutThumb()}
607 * and {@link #layoutTrack()} have both been called at least once.
608 *
609 * @param v The preview text view to measure.
610 * @param out Rectangle into which measured bounds are placed.
611 */
612 private void measurePreview(View v, Rect out) {
613 // Apply the preview image's padding as layout margins.
614 final Rect margins = mTempMargins;
615 margins.left = mPreviewImage.getPaddingLeft();
616 margins.top = mPreviewImage.getPaddingTop();
617 margins.right = mPreviewImage.getPaddingRight();
618 margins.bottom = mPreviewImage.getPaddingBottom();
619
Alan Viverettee8311ac2014-08-15 19:58:04 -0700620 if (mOverlayPosition == OVERLAY_FLOATING) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700621 measureFloating(v, margins, out);
Alan Viverettee8311ac2014-08-15 19:58:04 -0700622 } else {
623 measureViewToSide(v, mThumbImage, margins, out);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700624 }
625 }
626
627 /**
628 * Measures the bounds for a view that should be laid out against the edge
629 * of an adjacent view. If no adjacent view is provided, lays out against
630 * the list edge.
631 *
632 * @param view The view to measure for layout.
633 * @param adjacent (Optional) The adjacent view, may be null to align to the
634 * list edge.
635 * @param margins Layout margins to apply to the view.
636 * @param out Rectangle into which measured bounds are placed.
637 */
638 private void measureViewToSide(View view, View adjacent, Rect margins, Rect out) {
639 final int marginLeft;
640 final int marginTop;
641 final int marginRight;
642 if (margins == null) {
643 marginLeft = 0;
644 marginTop = 0;
645 marginRight = 0;
646 } else {
647 marginLeft = margins.left;
648 marginTop = margins.top;
649 marginRight = margins.right;
NoraBora9b38c602010-10-12 06:59:55 -0700650 }
Alan Viverettee918a482013-06-07 11:43:06 -0700651
Alan Viverettefb664152013-08-07 17:57:51 -0700652 final Rect container = mContainerRect;
653 final int containerWidth = container.width();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700654 final int maxWidth;
655 if (adjacent == null) {
Alan Viverettefb664152013-08-07 17:57:51 -0700656 maxWidth = containerWidth;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700657 } else if (mLayoutFromRight) {
658 maxWidth = adjacent.getLeft();
659 } else {
Alan Viverettefb664152013-08-07 17:57:51 -0700660 maxWidth = containerWidth - adjacent.getRight();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700661 }
Adam Powell20232d02010-12-08 21:08:53 -0800662
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700663 final int adjMaxWidth = maxWidth - marginLeft - marginRight;
664 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
665 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
666 view.measure(widthMeasureSpec, heightMeasureSpec);
Adam Powell20232d02010-12-08 21:08:53 -0800667
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700668 // Align to the left or right.
Alan Viverette8636ace2013-10-31 15:41:31 -0700669 final int width = Math.min(adjMaxWidth, view.getMeasuredWidth());
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700670 final int left;
671 final int right;
672 if (mLayoutFromRight) {
Alan Viverettefb664152013-08-07 17:57:51 -0700673 right = (adjacent == null ? container.right : adjacent.getLeft()) - marginRight;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700674 left = right - width;
675 } else {
Alan Viverettefb664152013-08-07 17:57:51 -0700676 left = (adjacent == null ? container.left : adjacent.getRight()) + marginLeft;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700677 right = left + width;
678 }
Adam Powellaf5280c2011-10-11 18:36:34 -0700679
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700680 // Don't adjust the vertical position.
681 final int top = marginTop;
682 final int bottom = top + view.getMeasuredHeight();
683 out.set(left, top, right, bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800684 }
Alan Viverettee918a482013-06-07 11:43:06 -0700685
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700686 private void measureFloating(View preview, Rect margins, Rect out) {
687 final int marginLeft;
688 final int marginTop;
689 final int marginRight;
690 if (margins == null) {
691 marginLeft = 0;
692 marginTop = 0;
693 marginRight = 0;
694 } else {
695 marginLeft = margins.left;
696 marginTop = margins.top;
697 marginRight = margins.right;
698 }
699
Alan Viverettefb664152013-08-07 17:57:51 -0700700 final Rect container = mContainerRect;
701 final int containerWidth = container.width();
702 final int adjMaxWidth = containerWidth - marginLeft - marginRight;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700703 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
704 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
705 preview.measure(widthMeasureSpec, heightMeasureSpec);
706
707 // Align at the vertical center, 10% from the top.
Alan Viverettefb664152013-08-07 17:57:51 -0700708 final int containerHeight = container.height();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700709 final int width = preview.getMeasuredWidth();
Alan Viverettefb664152013-08-07 17:57:51 -0700710 final int top = containerHeight / 10 + marginTop + container.top;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700711 final int bottom = top + preview.getMeasuredHeight();
Alan Viverettefb664152013-08-07 17:57:51 -0700712 final int left = (containerWidth - width) / 2 + container.left;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700713 final int right = left + width;
714 out.set(left, top, right, bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800715 }
Alan Viverettee918a482013-06-07 11:43:06 -0700716
Alan Viverette26bb2532013-08-09 10:40:50 -0700717 /**
718 * Updates the container rectangle used for layout.
719 */
Alan Viverettefb664152013-08-07 17:57:51 -0700720 private void updateContainerRect() {
721 final AbsListView list = mList;
Alan Viverette26bb2532013-08-09 10:40:50 -0700722 list.resolvePadding();
723
Alan Viverettefb664152013-08-07 17:57:51 -0700724 final Rect container = mContainerRect;
725 container.left = 0;
726 container.top = 0;
727 container.right = list.getWidth();
728 container.bottom = list.getHeight();
729
Alan Viverette26bb2532013-08-09 10:40:50 -0700730 final int scrollbarStyle = mScrollBarStyle;
Alan Viverettefb664152013-08-07 17:57:51 -0700731 if (scrollbarStyle == View.SCROLLBARS_INSIDE_INSET
732 || scrollbarStyle == View.SCROLLBARS_INSIDE_OVERLAY) {
733 container.left += list.getPaddingLeft();
734 container.top += list.getPaddingTop();
735 container.right -= list.getPaddingRight();
736 container.bottom -= list.getPaddingBottom();
Alan Viverette26bb2532013-08-09 10:40:50 -0700737
738 // In inset mode, we need to adjust for padded scrollbar width.
739 if (scrollbarStyle == View.SCROLLBARS_INSIDE_INSET) {
740 final int width = getWidth();
741 if (mScrollbarPosition == View.SCROLLBAR_POSITION_RIGHT) {
742 container.right += width;
743 } else {
744 container.left -= width;
745 }
746 }
Alan Viverettefb664152013-08-07 17:57:51 -0700747 }
748 }
749
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700750 /**
751 * Lays out the thumb according to the current scrollbar position.
752 */
753 private void layoutThumb() {
754 final Rect bounds = mTempBounds;
755 measureViewToSide(mThumbImage, null, null, bounds);
756 applyLayout(mThumbImage, bounds);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800757 }
Alan Viverettee918a482013-06-07 11:43:06 -0700758
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700759 /**
Alan Viverettefb664152013-08-07 17:57:51 -0700760 * Lays out the track centered on the thumb. Must be called after
761 * {@link #layoutThumb}.
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700762 */
763 private void layoutTrack() {
764 final View track = mTrackImage;
765 final View thumb = mThumbImage;
Alan Viverettefb664152013-08-07 17:57:51 -0700766 final Rect container = mContainerRect;
Alan Viverettea0f04942015-03-18 10:25:43 -0700767 final int maxWidth = container.width();
768 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700769 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
770 track.measure(widthMeasureSpec, heightMeasureSpec);
Alan Viverettee918a482013-06-07 11:43:06 -0700771
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700772 final int top;
773 final int bottom;
774 if (mThumbPosition == THUMB_POSITION_INSIDE) {
775 top = container.top;
776 bottom = container.bottom;
777 } else {
778 final int thumbHalfHeight = thumb.getHeight() / 2;
779 top = container.top + thumbHalfHeight;
780 bottom = container.bottom - thumbHalfHeight;
781 }
782
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700783 final int trackWidth = track.getMeasuredWidth();
Alan Viverettefb664152013-08-07 17:57:51 -0700784 final int left = thumb.getLeft() + (thumb.getWidth() - trackWidth) / 2;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700785 final int right = left + trackWidth;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700786 track.layout(left, top, right, bottom);
787 }
788
Alan Viverettee5f1fff2015-04-07 14:39:04 -0700789 /**
790 * Updates the offset and range used to convert from absolute y-position to
791 * thumb position within the track.
792 */
793 private void updateOffsetAndRange() {
794 final View trackImage = mTrackImage;
795 final View thumbImage = mThumbImage;
796 final float min;
797 final float max;
798 if (mThumbPosition == THUMB_POSITION_INSIDE) {
799 final float halfThumbHeight = thumbImage.getHeight() / 2f;
800 min = trackImage.getTop() + halfThumbHeight;
801 max = trackImage.getBottom() - halfThumbHeight;
802 } else{
803 min = trackImage.getTop();
804 max = trackImage.getBottom();
805 }
806
807 mThumbOffset = min;
808 mThumbRange = max - min;
809 }
810
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700811 private void setState(int state) {
812 mList.removeCallbacks(mDeferHide);
813
814 if (mAlwaysShow && state == STATE_NONE) {
815 state = STATE_VISIBLE;
816 }
817
818 if (state == mState) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800819 return;
820 }
821
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700822 switch (state) {
823 case STATE_NONE:
824 transitionToHidden();
825 break;
826 case STATE_VISIBLE:
827 transitionToVisible();
828 break;
829 case STATE_DRAGGING:
Alan Viverette6b40cc72013-06-25 16:41:52 -0700830 if (transitionPreviewLayout(mCurrentSection)) {
831 transitionToDragging();
832 } else {
833 transitionToVisible();
834 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700835 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800836 }
837
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700838 mState = state;
Adam Powell20232d02010-12-08 21:08:53 -0800839
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700840 refreshDrawablePressedState();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800841 }
842
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700843 private void refreshDrawablePressedState() {
844 final boolean isPressed = mState == STATE_DRAGGING;
845 mThumbImage.setPressed(isPressed);
846 mTrackImage.setPressed(isPressed);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800847 }
Adam Powell2c6196a2010-12-10 14:31:54 -0800848
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700849 /**
850 * Shows nothing.
851 */
852 private void transitionToHidden() {
853 if (mDecorAnimation != null) {
854 mDecorAnimation.cancel();
Adam Powell2c6196a2010-12-10 14:31:54 -0800855 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700856
857 final Animator fadeOut = groupAnimatorOfFloat(View.ALPHA, 0f, mThumbImage, mTrackImage,
858 mPreviewImage, mPrimaryText, mSecondaryText).setDuration(DURATION_FADE_OUT);
859
860 // Push the thumb and track outside the list bounds.
861 final float offset = mLayoutFromRight ? mThumbImage.getWidth() : -mThumbImage.getWidth();
862 final Animator slideOut = groupAnimatorOfFloat(
863 View.TRANSLATION_X, offset, mThumbImage, mTrackImage)
864 .setDuration(DURATION_FADE_OUT);
865
866 mDecorAnimation = new AnimatorSet();
867 mDecorAnimation.playTogether(fadeOut, slideOut);
868 mDecorAnimation.start();
Alan Viverettef9af7b22013-10-15 17:09:35 -0700869
870 mShowingPreview = false;
Adam Powell2c6196a2010-12-10 14:31:54 -0800871 }
872
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700873 /**
874 * Shows the thumb and track.
875 */
876 private void transitionToVisible() {
877 if (mDecorAnimation != null) {
878 mDecorAnimation.cancel();
879 }
880
881 final Animator fadeIn = groupAnimatorOfFloat(View.ALPHA, 1f, mThumbImage, mTrackImage)
882 .setDuration(DURATION_FADE_IN);
883 final Animator fadeOut = groupAnimatorOfFloat(
884 View.ALPHA, 0f, mPreviewImage, mPrimaryText, mSecondaryText)
885 .setDuration(DURATION_FADE_OUT);
886 final Animator slideIn = groupAnimatorOfFloat(
887 View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN);
888
889 mDecorAnimation = new AnimatorSet();
890 mDecorAnimation.playTogether(fadeIn, fadeOut, slideIn);
891 mDecorAnimation.start();
Alan Viverettef9af7b22013-10-15 17:09:35 -0700892
893 mShowingPreview = false;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700894 }
895
896 /**
897 * Shows the thumb, preview, and track.
898 */
899 private void transitionToDragging() {
900 if (mDecorAnimation != null) {
901 mDecorAnimation.cancel();
902 }
903
904 final Animator fadeIn = groupAnimatorOfFloat(
905 View.ALPHA, 1f, mThumbImage, mTrackImage, mPreviewImage)
906 .setDuration(DURATION_FADE_IN);
907 final Animator slideIn = groupAnimatorOfFloat(
908 View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN);
909
910 mDecorAnimation = new AnimatorSet();
911 mDecorAnimation.playTogether(fadeIn, slideIn);
912 mDecorAnimation.start();
Alan Viverettef9af7b22013-10-15 17:09:35 -0700913
914 mShowingPreview = true;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700915 }
916
Alan Viverettea709b372013-07-25 14:43:24 -0700917 private void postAutoHide() {
918 mList.removeCallbacks(mDeferHide);
919 mList.postDelayed(mDeferHide, FADE_TIMEOUT);
920 }
921
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700922 public void onScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) {
Alan Viveretteb9f27222013-09-06 19:39:47 -0700923 if (!isEnabled()) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700924 setState(STATE_NONE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800925 return;
926 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700927
928 final boolean hasMoreItems = totalItemCount - visibleItemCount > 0;
929 if (hasMoreItems && mState != STATE_DRAGGING) {
930 setThumbPos(getPosFromItemCount(firstVisibleItem, visibleItemCount, totalItemCount));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800931 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700932
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800933 mScrollCompleted = true;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700934
935 if (mFirstVisibleItem != firstVisibleItem) {
936 mFirstVisibleItem = firstVisibleItem;
937
938 // Show the thumb, if necessary, and set up auto-fade.
939 if (mState != STATE_DRAGGING) {
940 setState(STATE_VISIBLE);
Alan Viverettea709b372013-07-25 14:43:24 -0700941 postAutoHide();
Adam Powell20232d02010-12-08 21:08:53 -0800942 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800943 }
944 }
945
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700946 private void getSectionsFromIndexer() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800947 mSectionIndexer = null;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700948
949 Adapter adapter = mList.getAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800950 if (adapter instanceof HeaderViewListAdapter) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700951 mHeaderCount = ((HeaderViewListAdapter) adapter).getHeadersCount();
952 adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800953 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700954
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800955 if (adapter instanceof ExpandableListConnector) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700956 final ExpandableListAdapter expAdapter = ((ExpandableListConnector) adapter)
957 .getAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800958 if (expAdapter instanceof SectionIndexer) {
959 mSectionIndexer = (SectionIndexer) expAdapter;
Alan Viverette4f434c72013-12-13 11:01:21 -0800960 mListAdapter = adapter;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800961 mSections = mSectionIndexer.getSections();
962 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700963 } else if (adapter instanceof SectionIndexer) {
Alan Viverette4f434c72013-12-13 11:01:21 -0800964 mListAdapter = adapter;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700965 mSectionIndexer = (SectionIndexer) adapter;
966 mSections = mSectionIndexer.getSections();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800967 } else {
Alan Viverette4f434c72013-12-13 11:01:21 -0800968 mListAdapter = adapter;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700969 mSections = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800970 }
971 }
972
Adam Powellb1f498a2011-01-18 20:43:23 -0800973 public void onSectionsChanged() {
974 mListAdapter = null;
975 }
976
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700977 /**
978 * Scrolls to a specific position within the section
979 * @param position
980 */
981 private void scrollTo(float position) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800982 mScrollCompleted = false;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700983
984 final int count = mList.getCount();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800985 final Object[] sections = mSections;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700986 final int sectionCount = sections == null ? 0 : sections.length;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800987 int sectionIndex;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700988 if (sections != null && sectionCount > 1) {
989 final int exactSection = MathUtils.constrain(
990 (int) (position * sectionCount), 0, sectionCount - 1);
991 int targetSection = exactSection;
992 int targetIndex = mSectionIndexer.getPositionForSection(targetSection);
993 sectionIndex = targetSection;
994
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800995 // Given the expected section and index, the following code will
996 // try to account for missing sections (no names starting with..)
997 // It will compute the scroll space of surrounding empty sections
998 // and interpolate the currently visible letter's range across the
999 // available space, so that there is always some list movement while
1000 // the user moves the thumb.
1001 int nextIndex = count;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001002 int prevIndex = targetIndex;
1003 int prevSection = targetSection;
1004 int nextSection = targetSection + 1;
1005
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001006 // Assume the next section is unique
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001007 if (targetSection < sectionCount - 1) {
1008 nextIndex = mSectionIndexer.getPositionForSection(targetSection + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001009 }
Alan Viverettee918a482013-06-07 11:43:06 -07001010
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001011 // Find the previous index if we're slicing the previous section
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001012 if (nextIndex == targetIndex) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001013 // Non-existent letter
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001014 while (targetSection > 0) {
1015 targetSection--;
1016 prevIndex = mSectionIndexer.getPositionForSection(targetSection);
1017 if (prevIndex != targetIndex) {
1018 prevSection = targetSection;
1019 sectionIndex = targetSection;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07001020 break;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001021 } else if (targetSection == 0) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -07001022 // When section reaches 0 here, sectionIndex must follow it.
1023 // Assuming mSectionIndexer.getPositionForSection(0) == 0.
1024 sectionIndex = 0;
1025 break;
1026 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001027 }
1028 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001029
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001030 // Find the next index, in case the assumed next index is not
Alan Viverettee918a482013-06-07 11:43:06 -07001031 // unique. For instance, if there is no P, then request for P's
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001032 // position actually returns Q's. So we need to look ahead to make
Alan Viverettee918a482013-06-07 11:43:06 -07001033 // sure that there is really a Q at Q's position. If not, move
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001034 // further down...
1035 int nextNextSection = nextSection + 1;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001036 while (nextNextSection < sectionCount &&
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001037 mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
1038 nextNextSection++;
1039 nextSection++;
1040 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001041
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001042 // Compute the beginning and ending scroll range percentage of the
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001043 // currently visible section. This could be equal to or greater than
1044 // (1 / nSections). If the target position is near the previous
1045 // position, snap to the previous position.
1046 final float prevPosition = (float) prevSection / sectionCount;
1047 final float nextPosition = (float) nextSection / sectionCount;
1048 final float snapThreshold = (count == 0) ? Float.MAX_VALUE : .125f / count;
1049 if (prevSection == exactSection && position - prevPosition < snapThreshold) {
1050 targetIndex = prevIndex;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001051 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001052 targetIndex = prevIndex + (int) ((nextIndex - prevIndex) * (position - prevPosition)
1053 / (nextPosition - prevPosition));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001054 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001055
1056 // Clamp to valid positions.
1057 targetIndex = MathUtils.constrain(targetIndex, 0, count - 1);
Alan Viverettee918a482013-06-07 11:43:06 -07001058
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001059 if (mList instanceof ExpandableListView) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001060 final ExpandableListView expList = (ExpandableListView) mList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001061 expList.setSelectionFromTop(expList.getFlatListPosition(
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001062 ExpandableListView.getPackedPositionForGroup(targetIndex + mHeaderCount)),
1063 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001064 } else if (mList instanceof ListView) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001065 ((ListView) mList).setSelectionFromTop(targetIndex + mHeaderCount, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001066 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001067 mList.setSelection(targetIndex + mHeaderCount);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001068 }
1069 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001070 final int index = MathUtils.constrain((int) (position * count), 0, count - 1);
Adam Powell7ee1ff12011-03-09 16:35:13 -08001071
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001072 if (mList instanceof ExpandableListView) {
1073 ExpandableListView expList = (ExpandableListView) mList;
1074 expList.setSelectionFromTop(expList.getFlatListPosition(
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001075 ExpandableListView.getPackedPositionForGroup(index + mHeaderCount)), 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001076 } else if (mList instanceof ListView) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001077 ((ListView)mList).setSelectionFromTop(index + mHeaderCount, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001078 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001079 mList.setSelection(index + mHeaderCount);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001080 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001081
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001082 sectionIndex = -1;
1083 }
1084
Alan Viverette6b40cc72013-06-25 16:41:52 -07001085 if (mCurrentSection != sectionIndex) {
1086 mCurrentSection = sectionIndex;
1087
Alan Viverettef9af7b22013-10-15 17:09:35 -07001088 final boolean hasPreview = transitionPreviewLayout(sectionIndex);
1089 if (!mShowingPreview && hasPreview) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001090 transitionToDragging();
Alan Viverettef9af7b22013-10-15 17:09:35 -07001091 } else if (mShowingPreview && !hasPreview) {
Alan Viverette6b40cc72013-06-25 16:41:52 -07001092 transitionToVisible();
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001093 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001094 }
1095 }
1096
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001097 /**
Alan Viverette6b40cc72013-06-25 16:41:52 -07001098 * Transitions the preview text to a new section. Handles animation,
1099 * measurement, and layout. If the new preview text is empty, returns false.
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001100 *
Alan Viverette6b40cc72013-06-25 16:41:52 -07001101 * @param sectionIndex The section index to which the preview should
1102 * transition.
1103 * @return False if the new preview text is empty.
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001104 */
Alan Viverette6b40cc72013-06-25 16:41:52 -07001105 private boolean transitionPreviewLayout(int sectionIndex) {
1106 final Object[] sections = mSections;
1107 String text = null;
1108 if (sections != null && sectionIndex >= 0 && sectionIndex < sections.length) {
1109 final Object section = sections[sectionIndex];
1110 if (section != null) {
1111 text = section.toString();
1112 }
1113 }
1114
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001115 final Rect bounds = mTempBounds;
Alan Viverette8636ace2013-10-31 15:41:31 -07001116 final View preview = mPreviewImage;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001117 final TextView showing;
1118 final TextView target;
1119 if (mShowingPrimary) {
1120 showing = mPrimaryText;
1121 target = mSecondaryText;
1122 } else {
1123 showing = mSecondaryText;
1124 target = mPrimaryText;
1125 }
1126
1127 // Set and layout target immediately.
1128 target.setText(text);
1129 measurePreview(target, bounds);
1130 applyLayout(target, bounds);
1131
1132 if (mPreviewAnimation != null) {
1133 mPreviewAnimation.cancel();
1134 }
1135
1136 // Cross-fade preview text.
1137 final Animator showTarget = animateAlpha(target, 1f).setDuration(DURATION_CROSS_FADE);
1138 final Animator hideShowing = animateAlpha(showing, 0f).setDuration(DURATION_CROSS_FADE);
1139 hideShowing.addListener(mSwitchPrimaryListener);
1140
1141 // Apply preview image padding and animate bounds, if necessary.
Alan Viverette8636ace2013-10-31 15:41:31 -07001142 bounds.left -= preview.getPaddingLeft();
1143 bounds.top -= preview.getPaddingTop();
1144 bounds.right += preview.getPaddingRight();
1145 bounds.bottom += preview.getPaddingBottom();
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001146 final Animator resizePreview = animateBounds(preview, bounds);
1147 resizePreview.setDuration(DURATION_RESIZE);
1148
1149 mPreviewAnimation = new AnimatorSet();
1150 final AnimatorSet.Builder builder = mPreviewAnimation.play(hideShowing).with(showTarget);
1151 builder.with(resizePreview);
1152
1153 // The current preview size is unaffected by hidden or showing. It's
1154 // used to set starting scales for things that need to be scaled down.
1155 final int previewWidth = preview.getWidth() - preview.getPaddingLeft()
1156 - preview.getPaddingRight();
1157
1158 // If target is too large, shrink it immediately to fit and expand to
1159 // target size. Otherwise, start at target size.
1160 final int targetWidth = target.getWidth();
1161 if (targetWidth > previewWidth) {
1162 target.setScaleX((float) previewWidth / targetWidth);
1163 final Animator scaleAnim = animateScaleX(target, 1f).setDuration(DURATION_RESIZE);
1164 builder.with(scaleAnim);
1165 } else {
1166 target.setScaleX(1f);
1167 }
1168
1169 // If showing is larger than target, shrink to target size.
1170 final int showingWidth = showing.getWidth();
1171 if (showingWidth > targetWidth) {
1172 final float scale = (float) targetWidth / showingWidth;
1173 final Animator scaleAnim = animateScaleX(showing, scale).setDuration(DURATION_RESIZE);
1174 builder.with(scaleAnim);
1175 }
1176
1177 mPreviewAnimation.start();
Alan Viverette6b40cc72013-06-25 16:41:52 -07001178
Alan Viverettef9af7b22013-10-15 17:09:35 -07001179 return !TextUtils.isEmpty(text);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001180 }
1181
1182 /**
1183 * Positions the thumb and preview widgets.
1184 *
1185 * @param position The position, between 0 and 1, along the track at which
1186 * to place the thumb.
1187 */
1188 private void setThumbPos(float position) {
Alan Viverettee5f1fff2015-04-07 14:39:04 -07001189 final float thumbMiddle = position * mThumbRange + mThumbOffset;
1190 mThumbImage.setTranslationY(thumbMiddle - mThumbImage.getHeight() / 2f);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001191
Alan Viverette8636ace2013-10-31 15:41:31 -07001192 final View previewImage = mPreviewImage;
Alan Viverettefb664152013-08-07 17:57:51 -07001193 final float previewHalfHeight = previewImage.getHeight() / 2f;
Alan Viverettee8311ac2014-08-15 19:58:04 -07001194 final float previewPos;
1195 switch (mOverlayPosition) {
1196 case OVERLAY_AT_THUMB:
1197 previewPos = thumbMiddle;
1198 break;
1199 case OVERLAY_ABOVE_THUMB:
1200 previewPos = thumbMiddle - previewHalfHeight;
1201 break;
1202 case OVERLAY_FLOATING:
1203 default:
1204 previewPos = 0;
1205 break;
1206 }
1207
1208 // Center the preview on the thumb, constrained to the list bounds.
Alan Viverettee5f1fff2015-04-07 14:39:04 -07001209 final Rect container = mContainerRect;
1210 final int top = container.top;
1211 final int bottom = container.bottom;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001212 final float minP = top + previewHalfHeight;
1213 final float maxP = bottom - previewHalfHeight;
Adam Powell5db566f2013-10-13 17:19:10 -07001214 final float previewMiddle = MathUtils.constrain(previewPos, minP, maxP);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001215 final float previewTop = previewMiddle - previewHalfHeight;
Alan Viverettefb664152013-08-07 17:57:51 -07001216 previewImage.setTranslationY(previewTop);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001217
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001218 mPrimaryText.setTranslationY(previewTop);
1219 mSecondaryText.setTranslationY(previewTop);
1220 }
1221
1222 private float getPosFromMotionEvent(float y) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001223 // If the list is the same height as the thumbnail or shorter,
1224 // effectively disable scrolling.
Alan Viverettee5f1fff2015-04-07 14:39:04 -07001225 if (mThumbRange <= 0) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001226 return 0f;
1227 }
1228
Alan Viverettee5f1fff2015-04-07 14:39:04 -07001229 return MathUtils.constrain((y - mThumbOffset) / mThumbRange, 0f, 1f);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001230 }
1231
Alan Viverette827015e2014-11-13 10:26:53 -08001232 /**
1233 * Calculates the thumb position based on the visible items.
1234 *
1235 * @param firstVisibleItem First visible item, >= 0.
1236 * @param visibleItemCount Number of visible items, >= 0.
1237 * @param totalItemCount Total number of items, >= 0.
1238 * @return
1239 */
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001240 private float getPosFromItemCount(
1241 int firstVisibleItem, int visibleItemCount, int totalItemCount) {
Alan Viverette827015e2014-11-13 10:26:53 -08001242 final SectionIndexer sectionIndexer = mSectionIndexer;
1243 if (sectionIndexer == null || mListAdapter == null) {
Adam Powell32c3a692011-01-09 21:28:43 -08001244 getSectionsFromIndexer();
1245 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001246
Alan Viverette827015e2014-11-13 10:26:53 -08001247 if (visibleItemCount == 0 || totalItemCount == 0) {
1248 // No items are visible.
1249 return 0;
1250 }
1251
1252 final boolean hasSections = sectionIndexer != null && mSections != null
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001253 && mSections.length > 0;
1254 if (!hasSections || !mMatchDragPosition) {
Alan Viverette827015e2014-11-13 10:26:53 -08001255 if (visibleItemCount == totalItemCount) {
1256 // All items are visible.
1257 return 0;
1258 } else {
1259 return (float) firstVisibleItem / (totalItemCount - visibleItemCount);
1260 }
Adam Powell32c3a692011-01-09 21:28:43 -08001261 }
Alan Viverette827015e2014-11-13 10:26:53 -08001262
Alan Viverette6b40cc72013-06-25 16:41:52 -07001263 // Ignore headers.
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001264 firstVisibleItem -= mHeaderCount;
Adam Powell32c3a692011-01-09 21:28:43 -08001265 if (firstVisibleItem < 0) {
1266 return 0;
1267 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001268 totalItemCount -= mHeaderCount;
Adam Powell32c3a692011-01-09 21:28:43 -08001269
Alan Viverette6b40cc72013-06-25 16:41:52 -07001270 // Hidden portion of the first visible row.
1271 final View child = mList.getChildAt(0);
1272 final float incrementalPos;
1273 if (child == null || child.getHeight() == 0) {
1274 incrementalPos = 0;
1275 } else {
1276 incrementalPos = (float) (mList.getPaddingTop() - child.getTop()) / child.getHeight();
1277 }
1278
1279 // Number of rows in this section.
Alan Viverette827015e2014-11-13 10:26:53 -08001280 final int section = sectionIndexer.getSectionForPosition(firstVisibleItem);
1281 final int sectionPos = sectionIndexer.getPositionForSection(section);
Adam Powellf49971e2011-06-14 22:00:01 -07001282 final int sectionCount = mSections.length;
Alan Viverette6b40cc72013-06-25 16:41:52 -07001283 final int positionsInSection;
1284 if (section < sectionCount - 1) {
Jean-Baptiste Queru414b0232013-07-08 15:02:41 -07001285 final int nextSectionPos;
1286 if (section + 1 < sectionCount) {
Alan Viverette827015e2014-11-13 10:26:53 -08001287 nextSectionPos = sectionIndexer.getPositionForSection(section + 1);
Jean-Baptiste Queru414b0232013-07-08 15:02:41 -07001288 } else {
1289 nextSectionPos = totalItemCount - 1;
1290 }
Alan Viverette6b40cc72013-06-25 16:41:52 -07001291 positionsInSection = nextSectionPos - sectionPos;
1292 } else {
1293 positionsInSection = totalItemCount - sectionPos;
1294 }
Adam Powell32c3a692011-01-09 21:28:43 -08001295
Alan Viverette6b40cc72013-06-25 16:41:52 -07001296 // Position within this section.
1297 final float posWithinSection;
1298 if (positionsInSection == 0) {
1299 posWithinSection = 0;
1300 } else {
1301 posWithinSection = (firstVisibleItem + incrementalPos - sectionPos)
1302 / positionsInSection;
1303 }
1304
Alan Viverettef9af7b22013-10-15 17:09:35 -07001305 float result = (section + posWithinSection) / sectionCount;
1306
1307 // Fake out the scroll bar for the last item. Since the section indexer
1308 // won't ever actually move the list in this end space, make scrolling
1309 // across the last item account for whatever space is remaining.
1310 if (firstVisibleItem > 0 && firstVisibleItem + visibleItemCount == totalItemCount) {
1311 final View lastChild = mList.getChildAt(visibleItemCount - 1);
Yigit Boyar40c6c552014-09-18 16:01:28 -07001312 final int bottomPadding = mList.getPaddingBottom();
1313 final int maxSize;
1314 final int currentVisibleSize;
1315 if (mList.getClipToPadding()) {
1316 maxSize = lastChild.getHeight();
1317 currentVisibleSize = mList.getHeight() - bottomPadding - lastChild.getTop();
1318 } else {
1319 maxSize = lastChild.getHeight() + bottomPadding;
1320 currentVisibleSize = mList.getHeight() - lastChild.getTop();
1321 }
1322 if (currentVisibleSize > 0 && maxSize > 0) {
1323 result += (1 - result) * ((float) currentVisibleSize / maxSize );
1324 }
Alan Viverettef9af7b22013-10-15 17:09:35 -07001325 }
1326
1327 return result;
Adam Powell32c3a692011-01-09 21:28:43 -08001328 }
1329
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001330 /**
1331 * Cancels an ongoing fling event by injecting a
1332 * {@link MotionEvent#ACTION_CANCEL} into the host view.
1333 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001334 private void cancelFling() {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001335 final MotionEvent cancelFling = MotionEvent.obtain(
1336 0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001337 mList.onTouchEvent(cancelFling);
1338 cancelFling.recycle();
1339 }
Alan Viverettee918a482013-06-07 11:43:06 -07001340
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001341 /**
1342 * Cancels a pending drag.
1343 *
1344 * @see #startPendingDrag()
1345 */
1346 private void cancelPendingDrag() {
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001347 mPendingDrag = -1;
Adam Powellaf5280c2011-10-11 18:36:34 -07001348 }
1349
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001350 /**
1351 * Delays dragging until after the framework has determined that the user is
1352 * scrolling, rather than tapping.
1353 */
1354 private void startPendingDrag() {
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001355 mPendingDrag = SystemClock.uptimeMillis() + TAP_TIMEOUT;
Adam Powellaf5280c2011-10-11 18:36:34 -07001356 }
1357
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001358 private void beginDrag() {
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001359 mPendingDrag = -1;
1360
Adam Powellaf5280c2011-10-11 18:36:34 -07001361 setState(STATE_DRAGGING);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001362
Adam Powellaf5280c2011-10-11 18:36:34 -07001363 if (mListAdapter == null && mList != null) {
1364 getSectionsFromIndexer();
1365 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001366
Adam Powellaf5280c2011-10-11 18:36:34 -07001367 if (mList != null) {
1368 mList.requestDisallowInterceptTouchEvent(true);
1369 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
1370 }
1371
1372 cancelFling();
1373 }
1374
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001375 public boolean onInterceptTouchEvent(MotionEvent ev) {
Alan Viveretteb9f27222013-09-06 19:39:47 -07001376 if (!isEnabled()) {
Alan Viverette447cdf22013-07-15 17:47:34 -07001377 return false;
1378 }
1379
Adam Powellaf5280c2011-10-11 18:36:34 -07001380 switch (ev.getActionMasked()) {
1381 case MotionEvent.ACTION_DOWN:
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001382 if (isPointInside(ev.getX(), ev.getY())) {
1383 // If the parent has requested that its children delay
1384 // pressed state (e.g. is a scrolling container) then we
1385 // need to allow the parent time to decide whether it wants
1386 // to intercept events. If it does, we will receive a CANCEL
1387 // event.
Alan Viveretteb9f27222013-09-06 19:39:47 -07001388 if (!mList.isInScrollingContainer()) {
1389 beginDrag();
1390 return true;
Adam Powellaf5280c2011-10-11 18:36:34 -07001391 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001392
Alan Viveretteb9f27222013-09-06 19:39:47 -07001393 mInitialTouchY = ev.getY();
1394 startPendingDrag();
1395 }
1396 break;
1397 case MotionEvent.ACTION_MOVE:
1398 if (!isPointInside(ev.getX(), ev.getY())) {
1399 cancelPendingDrag();
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001400 } else if (mPendingDrag >= 0 && mPendingDrag <= SystemClock.uptimeMillis()) {
1401 beginDrag();
1402
1403 final float pos = getPosFromMotionEvent(mInitialTouchY);
1404 scrollTo(pos);
1405
1406 return onTouchEvent(ev);
Adam Powellaf5280c2011-10-11 18:36:34 -07001407 }
1408 break;
1409 case MotionEvent.ACTION_UP:
1410 case MotionEvent.ACTION_CANCEL:
1411 cancelPendingDrag();
1412 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001413 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001414
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001415 return false;
1416 }
1417
Alan Viverettea709b372013-07-25 14:43:24 -07001418 public boolean onInterceptHoverEvent(MotionEvent ev) {
Alan Viveretteb9f27222013-09-06 19:39:47 -07001419 if (!isEnabled()) {
Alan Viverettea709b372013-07-25 14:43:24 -07001420 return false;
1421 }
1422
1423 final int actionMasked = ev.getActionMasked();
1424 if ((actionMasked == MotionEvent.ACTION_HOVER_ENTER
1425 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) && mState == STATE_NONE
1426 && isPointInside(ev.getX(), ev.getY())) {
1427 setState(STATE_VISIBLE);
1428 postAutoHide();
1429 }
1430
1431 return false;
1432 }
1433
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001434 public boolean onTouchEvent(MotionEvent me) {
Alan Viveretteb9f27222013-09-06 19:39:47 -07001435 if (!isEnabled()) {
Alan Viverette447cdf22013-07-15 17:47:34 -07001436 return false;
1437 }
1438
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001439 switch (me.getActionMasked()) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001440 case MotionEvent.ACTION_UP: {
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001441 if (mPendingDrag >= 0) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001442 // Allow a tap to scroll.
1443 beginDrag();
Adam Powellaf5280c2011-10-11 18:36:34 -07001444
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001445 final float pos = getPosFromMotionEvent(me.getY());
1446 setThumbPos(pos);
1447 scrollTo(pos);
1448
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001449 // Will hit the STATE_DRAGGING check below
Adam Powell20232d02010-12-08 21:08:53 -08001450 }
1451
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001452 if (mState == STATE_DRAGGING) {
1453 if (mList != null) {
1454 // ViewGroup does the right thing already, but there might
1455 // be other classes that don't properly reset on touch-up,
1456 // so do this explicitly just in case.
1457 mList.requestDisallowInterceptTouchEvent(false);
1458 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1459 }
1460
1461 setState(STATE_VISIBLE);
Alan Viverettea709b372013-07-25 14:43:24 -07001462 postAutoHide();
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001463
1464 return true;
1465 }
1466 } break;
1467
1468 case MotionEvent.ACTION_MOVE: {
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001469 if (mPendingDrag >= 0 && Math.abs(me.getY() - mInitialTouchY) > mScaledTouchSlop) {
1470 beginDrag();
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001471
Adam Powellaf5280c2011-10-11 18:36:34 -07001472 // Will hit the STATE_DRAGGING check below
1473 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001474
1475 if (mState == STATE_DRAGGING) {
1476 // TODO: Ignore jitter.
1477 final float pos = getPosFromMotionEvent(me.getY());
1478 setThumbPos(pos);
1479
1480 // If the previous scrollTo is still pending
1481 if (mScrollCompleted) {
1482 scrollTo(pos);
1483 }
1484
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001485 return true;
1486 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001487 } break;
1488
1489 case MotionEvent.ACTION_CANCEL: {
1490 cancelPendingDrag();
1491 } break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001492 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001493
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001494 return false;
1495 }
Romain Guyd6a463a2009-05-21 23:10:10 -07001496
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001497 /**
1498 * Returns whether a coordinate is inside the scroller's activation area. If
1499 * there is a track image, touching anywhere within the thumb-width of the
1500 * track activates scrolling. Otherwise, the user has to touch inside thumb
1501 * itself.
1502 *
1503 * @param x The x-coordinate.
1504 * @param y The y-coordinate.
1505 * @return Whether the coordinate is inside the scroller's activation area.
1506 */
1507 private boolean isPointInside(float x, float y) {
Alan Viverette8636ace2013-10-31 15:41:31 -07001508 return isPointInsideX(x) && (mTrackDrawable != null || isPointInsideY(y));
Romain Guy82f34952009-05-24 18:40:45 -07001509 }
1510
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001511 private boolean isPointInsideX(float x) {
Alan Viverette7d5bcd72014-11-20 16:07:59 -08001512 final float offset = mThumbImage.getTranslationX();
1513 final float left = mThumbImage.getLeft() + offset;
1514 final float right = mThumbImage.getRight() + offset;
1515
1516 // Apply the minimum touch target size.
1517 final float targetSizeDiff = mMinimumTouchTarget - (right - left);
1518 final float adjust = targetSizeDiff > 0 ? targetSizeDiff : 0;
1519
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001520 if (mLayoutFromRight) {
Alan Viverette7d5bcd72014-11-20 16:07:59 -08001521 return x >= mThumbImage.getLeft() - adjust;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001522 } else {
Alan Viverette7d5bcd72014-11-20 16:07:59 -08001523 return x <= mThumbImage.getRight() + adjust;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001524 }
1525 }
Alan Viverettee918a482013-06-07 11:43:06 -07001526
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001527 private boolean isPointInsideY(float y) {
Adam Powelld8273892013-10-13 13:35:27 -07001528 final float offset = mThumbImage.getTranslationY();
1529 final float top = mThumbImage.getTop() + offset;
1530 final float bottom = mThumbImage.getBottom() + offset;
Alan Viverette7d5bcd72014-11-20 16:07:59 -08001531
1532 // Apply the minimum touch target size.
1533 final float targetSizeDiff = mMinimumTouchTarget - (bottom - top);
1534 final float adjust = targetSizeDiff > 0 ? targetSizeDiff / 2 : 0;
1535
1536 return y >= (top - adjust) && y <= (bottom + adjust);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001537 }
Alan Viverettee918a482013-06-07 11:43:06 -07001538
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001539 /**
1540 * Constructs an animator for the specified property on a group of views.
1541 * See {@link ObjectAnimator#ofFloat(Object, String, float...)} for
1542 * implementation details.
1543 *
1544 * @param property The property being animated.
1545 * @param value The value to which that property should animate.
1546 * @param views The target views to animate.
1547 * @return An animator for all the specified views.
1548 */
1549 private static Animator groupAnimatorOfFloat(
1550 Property<View, Float> property, float value, View... views) {
1551 AnimatorSet animSet = new AnimatorSet();
1552 AnimatorSet.Builder builder = null;
1553
1554 for (int i = views.length - 1; i >= 0; i--) {
1555 final Animator anim = ObjectAnimator.ofFloat(views[i], property, value);
1556 if (builder == null) {
1557 builder = animSet.play(anim);
1558 } else {
1559 builder.with(anim);
1560 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001561 }
Alan Viverettee918a482013-06-07 11:43:06 -07001562
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001563 return animSet;
1564 }
1565
1566 /**
1567 * Returns an animator for the view's scaleX value.
1568 */
1569 private static Animator animateScaleX(View v, float target) {
1570 return ObjectAnimator.ofFloat(v, View.SCALE_X, target);
1571 }
1572
1573 /**
1574 * Returns an animator for the view's alpha value.
1575 */
1576 private static Animator animateAlpha(View v, float alpha) {
1577 return ObjectAnimator.ofFloat(v, View.ALPHA, alpha);
1578 }
1579
1580 /**
1581 * A Property wrapper around the <code>left</code> functionality handled by the
1582 * {@link View#setLeft(int)} and {@link View#getLeft()} methods.
1583 */
1584 private static Property<View, Integer> LEFT = new IntProperty<View>("left") {
1585 @Override
1586 public void setValue(View object, int value) {
1587 object.setLeft(value);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001588 }
Alan Viverettee918a482013-06-07 11:43:06 -07001589
1590 @Override
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001591 public Integer get(View object) {
1592 return object.getLeft();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001593 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001594 };
1595
1596 /**
1597 * A Property wrapper around the <code>top</code> functionality handled by the
1598 * {@link View#setTop(int)} and {@link View#getTop()} methods.
1599 */
1600 private static Property<View, Integer> TOP = new IntProperty<View>("top") {
1601 @Override
1602 public void setValue(View object, int value) {
1603 object.setTop(value);
1604 }
1605
1606 @Override
1607 public Integer get(View object) {
1608 return object.getTop();
1609 }
1610 };
1611
1612 /**
1613 * A Property wrapper around the <code>right</code> functionality handled by the
1614 * {@link View#setRight(int)} and {@link View#getRight()} methods.
1615 */
1616 private static Property<View, Integer> RIGHT = new IntProperty<View>("right") {
1617 @Override
1618 public void setValue(View object, int value) {
1619 object.setRight(value);
1620 }
1621
1622 @Override
1623 public Integer get(View object) {
1624 return object.getRight();
1625 }
1626 };
1627
1628 /**
1629 * A Property wrapper around the <code>bottom</code> functionality handled by the
1630 * {@link View#setBottom(int)} and {@link View#getBottom()} methods.
1631 */
1632 private static Property<View, Integer> BOTTOM = new IntProperty<View>("bottom") {
1633 @Override
1634 public void setValue(View object, int value) {
1635 object.setBottom(value);
1636 }
1637
1638 @Override
1639 public Integer get(View object) {
1640 return object.getBottom();
1641 }
1642 };
1643
1644 /**
1645 * Returns an animator for the view's bounds.
1646 */
1647 private static Animator animateBounds(View v, Rect bounds) {
1648 final PropertyValuesHolder left = PropertyValuesHolder.ofInt(LEFT, bounds.left);
1649 final PropertyValuesHolder top = PropertyValuesHolder.ofInt(TOP, bounds.top);
1650 final PropertyValuesHolder right = PropertyValuesHolder.ofInt(RIGHT, bounds.right);
1651 final PropertyValuesHolder bottom = PropertyValuesHolder.ofInt(BOTTOM, bounds.bottom);
1652 return ObjectAnimator.ofPropertyValuesHolder(v, left, top, right, bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001653 }
1654}