blob: fe143dec8900845141850bc9df87a4a144e220da [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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025import android.content.Context;
26import android.content.res.ColorStateList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027import android.content.res.TypedArray;
Adam Powell20232d02010-12-08 21:08:53 -080028import android.graphics.Rect;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import android.graphics.drawable.Drawable;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070030import android.os.Build;
Alan Viverette4cfeedb2013-12-03 14:03:24 -080031import android.os.SystemClock;
Alan Viverettef9af7b22013-10-15 17:09:35 -070032import android.text.TextUtils;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070033import android.text.TextUtils.TruncateAt;
34import android.util.IntProperty;
35import android.util.MathUtils;
36import android.util.Property;
Alan Viverette7b630632013-10-04 11:44:51 -070037import android.util.TypedValue;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070038import android.view.Gravity;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080039import android.view.MotionEvent;
Adam Powell20232d02010-12-08 21:08:53 -080040import android.view.View;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070041import android.view.View.MeasureSpec;
Adam Powellaf5280c2011-10-11 18:36:34 -070042import android.view.ViewConfiguration;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070043import android.view.ViewGroup.LayoutParams;
44import android.view.ViewGroupOverlay;
Adam Powelld43bd482010-02-26 16:29:09 -080045import android.widget.AbsListView.OnScrollListener;
Alan Viverette8636ace2013-10-31 15:41:31 -070046import android.widget.ImageView.ScaleType;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070047
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080048/**
49 * Helper class for AbsListView to draw and control the Fast Scroll thumb
50 */
51class FastScroller {
Alan Viverette0ebe81e2013-06-21 17:01:36 -070052 /** Duration of fade-out animation. */
53 private static final int DURATION_FADE_OUT = 300;
Alan Viverettee918a482013-06-07 11:43:06 -070054
Alan Viverette0ebe81e2013-06-21 17:01:36 -070055 /** 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. */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080071 private static final int STATE_NONE = 0;
Adam Powell20232d02010-12-08 21:08:53 -080072
Alan Viverette0ebe81e2013-06-21 17:01:36 -070073 /** Scroll thumb visible and moving along with the scrollbar. */
74 private static final int STATE_VISIBLE = 1;
Adam Powell20232d02010-12-08 21:08:53 -080075
Alan Viverette0ebe81e2013-06-21 17:01:36 -070076 /** Scroll thumb and preview being dragged by user. */
77 private static final int STATE_DRAGGING = 2;
Adam Powell20232d02010-12-08 21:08:53 -080078
Alan Viverette0ebe81e2013-06-21 17:01:36 -070079 // Positions for preview image and text.
Adam Powell20232d02010-12-08 21:08:53 -080080 private static final int OVERLAY_FLOATING = 0;
81 private static final int OVERLAY_AT_THUMB = 1;
Alan Viverettee8311ac2014-08-15 19:58:04 -070082 private static final int OVERLAY_ABOVE_THUMB = 2;
Alan Viverettee918a482013-06-07 11:43:06 -070083
Alan Viverette0ebe81e2013-06-21 17:01:36 -070084 // Indices for mPreviewResId.
85 private static final int PREVIEW_LEFT = 0;
86 private static final int PREVIEW_RIGHT = 1;
Adam Powell20232d02010-12-08 21:08:53 -080087
Alan Viverette0ebe81e2013-06-21 17:01:36 -070088 /** Delay before considering a tap in the thumb area to be a drag. */
89 private static final long TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080090
Alan Viverette0ebe81e2013-06-21 17:01:36 -070091 private final Rect mTempBounds = new Rect();
92 private final Rect mTempMargins = new Rect();
Alan Viverettefb664152013-08-07 17:57:51 -070093 private final Rect mContainerRect = new Rect();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080094
Alan Viverette0ebe81e2013-06-21 17:01:36 -070095 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;
Alan Viverette8636ace2013-10-31 15:41:31 -0700101 private final View mPreviewImage;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800102
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700103 /**
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
Alan Viverette7d5bcd72014-11-20 16:07:59 -0800109 /** The minimum touch target size in pixels. */
110 private final int mMinimumTouchTarget;
111
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700112 /**
113 * Padding in pixels around the preview text. Applied as layout margins to
114 * the preview text and padding to the preview image.
115 */
Alan Viverette8636ace2013-10-31 15:41:31 -0700116 private int mPreviewPadding;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700117
Alan Viverette8636ace2013-10-31 15:41:31 -0700118 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;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700132
Alan Viverette26bb2532013-08-09 10:40:50 -0700133 /** Total width of decorations. */
Alan Viverette8636ace2013-10-31 15:41:31 -0700134 private int mWidth;
Alan Viverette26bb2532013-08-09 10:40:50 -0700135
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700136 /** 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
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700154 /** The index of the current section. */
155 private int mCurrentSection = -1;
156
Alan Viverette26bb2532013-08-09 10:40:50 -0700157 /** The current scrollbar position. */
158 private int mScrollbarPosition = -1;
159
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700160 /** Whether the list is long enough to need a fast scroller. */
The Android Open Source Project4df24232009-03-05 14:34:35 -0800161 private boolean mLongList;
Alan Viverettee918a482013-06-07 11:43:06 -0700162
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700163 private Object[] mSections;
Alan Viverettee918a482013-06-07 11:43:06 -0700164
Alan Viverettefb664152013-08-07 17:57:51 -0700165 /** Whether this view is currently performing layout. */
166 private boolean mUpdatingLayout;
167
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700168 /**
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 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800176 private int mState;
Alan Viverettee918a482013-06-07 11:43:06 -0700177
Alan Viverettef9af7b22013-10-15 17:09:35 -0700178 /** Whether the preview image is visible. */
179 private boolean mShowingPreview;
180
Alan Viverette4f434c72013-12-13 11:01:21 -0800181 private Adapter mListAdapter;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800182 private SectionIndexer mSectionIndexer;
183
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700184 /** Whether decorations should be laid out from right to left. */
185 private boolean mLayoutFromRight;
Alan Viverettee918a482013-06-07 11:43:06 -0700186
Alan Viverette447cdf22013-07-15 17:47:34 -0700187 /** Whether the fast scroller is enabled. */
188 private boolean mEnabled;
189
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700190 /** Whether the scrollbar and decorations should always be shown. */
Adam Powell20232d02010-12-08 21:08:53 -0800191 private boolean mAlwaysShow;
192
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700193 /**
194 * Position for the preview image and text. One of:
195 * <ul>
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700196 * <li>{@link #OVERLAY_FLOATING}
Alan Viverettee8311ac2014-08-15 19:58:04 -0700197 * <li>{@link #OVERLAY_AT_THUMB}
198 * <li>{@link #OVERLAY_ABOVE_THUMB}
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700199 * </ul>
200 */
Adam Powell20232d02010-12-08 21:08:53 -0800201 private int mOverlayPosition;
202
Alan Viverette26bb2532013-08-09 10:40:50 -0700203 /** Current scrollbar style, including inset and overlay properties. */
204 private int mScrollBarStyle;
205
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700206 /** Whether to precisely match the thumb position to the list. */
Adam Powell568ccd82011-08-03 22:38:48 -0700207 private boolean mMatchDragPosition;
208
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700209 private float mInitialTouchY;
Alan Viverette4cfeedb2013-12-03 14:03:24 -0800210 private long mPendingDrag = -1;
Adam Powellaf5280c2011-10-11 18:36:34 -0700211 private int mScaledTouchSlop;
212
Alan Viverette4b95cc72014-01-14 16:54:02 -0800213 private int mOldItemCount;
214 private int mOldChildCount;
215
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700216 /**
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;
Adam Powellaf5280c2011-10-11 18:36:34 -0700233 }
234 };
235
Alan Viverette8636ace2013-10-31 15:41:31 -0700236 public FastScroller(AbsListView listView, int styleResId) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800237 mList = listView;
Alan Viverette4b95cc72014-01-14 16:54:02 -0800238 mOldItemCount = listView.getCount();
239 mOldChildCount = listView.getChildCount();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700240
Alan Viverette39bed692013-08-07 15:47:04 -0700241 final Context context = listView.getContext();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700242 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
Alan Viverette8636ace2013-10-31 15:41:31 -0700243 mScrollBarStyle = listView.getScrollBarStyle();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700244
Alan Viverette8636ace2013-10-31 15:41:31 -0700245 mScrollCompleted = true;
246 mState = STATE_VISIBLE;
247 mMatchDragPosition =
248 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700249
Alan Viverette8636ace2013-10-31 15:41:31 -0700250 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);
Alan Viverette26bb2532013-08-09 10:40:50 -0700256
Alan Viverette8636ace2013-10-31 15:41:31 -0700257 mPrimaryText = createPreviewTextView(context);
258 mSecondaryText = createPreviewTextView(context);
259
Alan Viverette7d5bcd72014-11-20 16:07:59 -0800260 mMinimumTouchTarget = listView.getResources().getDimensionPixelSize(
261 com.android.internal.R.dimen.fast_scroller_minimum_touch_target);
262
Alan Viverette8636ace2013-10-31 15:41:31 -0700263 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();
Alan Viverette4b95cc72014-01-14 16:54:02 -0800274 updateLongList(mOldChildCount, mOldItemCount);
Alan Viverette8636ace2013-10-31 15:41:31 -0700275 setScrollbarPosition(listView.getVerticalScrollbarPosition());
276 postAutoHide();
277 }
278
279 private void updateAppearance() {
280 final Context context = mList.getContext();
Alan Viverette26bb2532013-08-09 10:40:50 -0700281 int width = 0;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700282
283 // Add track to overlay if it has an image.
Alan Viverette8636ace2013-10-31 15:41:31 -0700284 mTrackImage.setImageDrawable(mTrackDrawable);
285 if (mTrackDrawable != null) {
286 width = Math.max(width, mTrackDrawable.getIntrinsicWidth());
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700287 }
288
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700289 // Add thumb to overlay if it has an image.
Alan Viverette8636ace2013-10-31 15:41:31 -0700290 mThumbImage.setImageDrawable(mThumbDrawable);
291 mThumbImage.setMinimumWidth(mThumbMinWidth);
292 mThumbImage.setMinimumHeight(mThumbMinHeight);
293 if (mThumbDrawable != null) {
294 width = Math.max(width, mThumbDrawable.getIntrinsicWidth());
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700295 }
296
Alan Viverette8636ace2013-10-31 15:41:31 -0700297 // 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);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700306 }
307
Alan Viverette8636ace2013-10-31 15:41:31 -0700308 if (mTextColor != null) {
309 mPrimaryText.setTextColor(mTextColor);
310 mSecondaryText.setTextColor(mTextColor);
311 }
Alan Viverette26bb2532013-08-09 10:40:50 -0700312
Alan Viverette8636ace2013-10-31 15:41:31 -0700313 if (mTextSize > 0) {
314 mPrimaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
315 mSecondaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
316 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700317
Alan Viverette8636ace2013-10-31 15:41:31 -0700318 final int textMinSize = Math.max(0, mPreviewMinHeight);
Alan Viverette6b40cc72013-06-25 16:41:52 -0700319 mPrimaryText.setMinimumWidth(textMinSize);
320 mPrimaryText.setMinimumHeight(textMinSize);
Alan Viverettee8311ac2014-08-15 19:58:04 -0700321 mPrimaryText.setIncludeFontPadding(false);
Alan Viverette6b40cc72013-06-25 16:41:52 -0700322 mSecondaryText.setMinimumWidth(textMinSize);
323 mSecondaryText.setMinimumHeight(textMinSize);
Alan Viverettee8311ac2014-08-15 19:58:04 -0700324 mSecondaryText.setIncludeFontPadding(false);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700325
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700326 refreshDrawablePressedState();
Alan Viverette8636ace2013-10-31 15:41:31 -0700327 }
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();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800380 }
381
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700382 /**
Alan Viverette447cdf22013-07-15 17:47:34 -0700383 * 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) {
Alan Viveretteb9f27222013-09-06 19:39:47 -0700397 if (mEnabled != enabled) {
398 mEnabled = enabled;
Alan Viverette447cdf22013-07-15 17:47:34 -0700399
Alan Viverette8ac22b02013-12-12 18:35:40 -0800400 onStateDependencyChanged(true);
Alan Viverette447cdf22013-07-15 17:47:34 -0700401 }
402 }
403
404 /**
405 * @return Whether the fast scroll thumb is enabled.
406 */
407 public boolean isEnabled() {
Alan Viveretteb9f27222013-09-06 19:39:47 -0700408 return mEnabled && (mLongList || mAlwaysShow);
Alan Viverette447cdf22013-07-15 17:47:34 -0700409 }
410
411 /**
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700412 * @param alwaysShow Whether the fast scroll thumb should always be shown
413 */
Adam Powell20232d02010-12-08 21:08:53 -0800414 public void setAlwaysShow(boolean alwaysShow) {
Alan Viveretteb9f27222013-09-06 19:39:47 -0700415 if (mAlwaysShow != alwaysShow) {
416 mAlwaysShow = alwaysShow;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700417
Alan Viverette8ac22b02013-12-12 18:35:40 -0800418 onStateDependencyChanged(false);
Adam Powell20232d02010-12-08 21:08:53 -0800419 }
420 }
421
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700422 /**
423 * @return Whether the fast scroll thumb will always be shown
424 * @see #setAlwaysShow(boolean)
425 */
Adam Powell20232d02010-12-08 21:08:53 -0800426 public boolean isAlwaysShowEnabled() {
427 return mAlwaysShow;
428 }
429
Alan Viveretteb9f27222013-09-06 19:39:47 -0700430 /**
431 * Called when one of the variables affecting enabled state changes.
Alan Viverette8ac22b02013-12-12 18:35:40 -0800432 *
433 * @param peekIfEnabled whether the thumb should peek, if enabled
Alan Viveretteb9f27222013-09-06 19:39:47 -0700434 */
Alan Viverette8ac22b02013-12-12 18:35:40 -0800435 private void onStateDependencyChanged(boolean peekIfEnabled) {
Alan Viveretteb9f27222013-09-06 19:39:47 -0700436 if (isEnabled()) {
437 if (isAlwaysShowEnabled()) {
438 setState(STATE_VISIBLE);
439 } else if (mState == STATE_VISIBLE) {
440 postAutoHide();
Alan Viverette8ac22b02013-12-12 18:35:40 -0800441 } else if (peekIfEnabled) {
442 setState(STATE_VISIBLE);
443 postAutoHide();
Alan Viveretteb9f27222013-09-06 19:39:47 -0700444 }
445 } else {
446 stop();
447 }
448
449 mList.resolvePadding();
450 }
451
Alan Viverette26bb2532013-08-09 10:40:50 -0700452 public void setScrollBarStyle(int style) {
453 if (mScrollBarStyle != style) {
454 mScrollBarStyle = style;
455
456 updateLayout();
457 }
458 }
459
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700460 /**
461 * Immediately transitions the fast scroller decorations to a hidden state.
462 */
463 public void stop() {
464 setState(STATE_NONE);
465 }
Adam Powell20232d02010-12-08 21:08:53 -0800466
Adam Powell20232d02010-12-08 21:08:53 -0800467 public void setScrollbarPosition(int position) {
Fabrice Di Meglioc23ee462012-06-22 18:46:06 -0700468 if (position == View.SCROLLBAR_POSITION_DEFAULT) {
469 position = mList.isLayoutRtl() ?
470 View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT;
471 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700472
Alan Viverette26bb2532013-08-09 10:40:50 -0700473 if (mScrollbarPosition != position) {
474 mScrollbarPosition = position;
475 mLayoutFromRight = position != View.SCROLLBAR_POSITION_LEFT;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700476
Alan Viverette26bb2532013-08-09 10:40:50 -0700477 final int previewResId = mPreviewResId[mLayoutFromRight ? PREVIEW_RIGHT : PREVIEW_LEFT];
478 mPreviewImage.setBackgroundResource(previewResId);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700479
Alan Viverette26bb2532013-08-09 10:40:50 -0700480 // 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();
Adam Powell20232d02010-12-08 21:08:53 -0800491 }
492 }
493
494 public int getWidth() {
Alan Viverette26bb2532013-08-09 10:40:50 -0700495 return mWidth;
Adam Powell20232d02010-12-08 21:08:53 -0800496 }
497
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700498 public void onSizeChanged(int w, int h, int oldw, int oldh) {
499 updateLayout();
500 }
501
Alan Viverette4b95cc72014-01-14 16:54:02 -0800502 public void onItemCountChanged(int childCount, int itemCount) {
503 if (mOldItemCount != itemCount || mOldChildCount != childCount) {
504 mOldItemCount = itemCount;
505 mOldChildCount = childCount;
Alan Viveretteb9f27222013-09-06 19:39:47 -0700506
Alan Viverette4b95cc72014-01-14 16:54:02 -0800507 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 }
Alan Viveretteb9f27222013-09-06 19:39:47 -0700515 }
516
Alan Viverette4b95cc72014-01-14 16:54:02 -0800517 private void updateLongList(int childCount, int itemCount) {
518 final boolean longList = childCount > 0 && itemCount / childCount >= MIN_PAGES;
Alan Viveretteb9f27222013-09-06 19:39:47 -0700519 if (mLongList != longList) {
520 mLongList = longList;
521
Alan Viverette8ac22b02013-12-12 18:35:40 -0800522 onStateDependencyChanged(false);
Alan Viveretteb9f27222013-09-06 19:39:47 -0700523 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800524 }
Alan Viverettee918a482013-06-07 11:43:06 -0700525
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700526 /**
527 * Creates a view into which preview text can be placed.
528 */
Alan Viverette8636ace2013-10-31 15:41:31 -0700529 private TextView createPreviewTextView(Context context) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700530 final LayoutParams params = new LayoutParams(
531 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700532 final TextView textView = new TextView(context);
533 textView.setLayoutParams(params);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700534 textView.setSingleLine(true);
535 textView.setEllipsize(TruncateAt.MIDDLE);
536 textView.setGravity(Gravity.CENTER);
537 textView.setAlpha(0f);
Adam Powell20232d02010-12-08 21:08:53 -0800538
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700539 // Manually propagate inherited layout direction.
540 textView.setLayoutDirection(mList.getLayoutDirection());
NoraBora9b38c602010-10-12 06:59:55 -0700541
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700542 return textView;
543 }
544
545 /**
546 * Measures and layouts the scrollbar and decorations.
547 */
Alan Viverette26bb2532013-08-09 10:40:50 -0700548 public void updateLayout() {
Alan Viverettefb664152013-08-07 17:57:51 -0700549 // 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
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700559 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 }
Alan Viverettefb664152013-08-07 17:57:51 -0700576
577 mUpdatingLayout = false;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700578 }
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
Alan Viverettee8311ac2014-08-15 19:58:04 -0700608 if (mOverlayPosition == OVERLAY_FLOATING) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700609 measureFloating(v, margins, out);
Alan Viverettee8311ac2014-08-15 19:58:04 -0700610 } else {
611 measureViewToSide(v, mThumbImage, margins, out);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700612 }
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;
NoraBora9b38c602010-10-12 06:59:55 -0700638 }
Alan Viverettee918a482013-06-07 11:43:06 -0700639
Alan Viverettefb664152013-08-07 17:57:51 -0700640 final Rect container = mContainerRect;
641 final int containerWidth = container.width();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700642 final int maxWidth;
643 if (adjacent == null) {
Alan Viverettefb664152013-08-07 17:57:51 -0700644 maxWidth = containerWidth;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700645 } else if (mLayoutFromRight) {
646 maxWidth = adjacent.getLeft();
647 } else {
Alan Viverettefb664152013-08-07 17:57:51 -0700648 maxWidth = containerWidth - adjacent.getRight();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700649 }
Adam Powell20232d02010-12-08 21:08:53 -0800650
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700651 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);
Adam Powell20232d02010-12-08 21:08:53 -0800655
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700656 // Align to the left or right.
Alan Viverette8636ace2013-10-31 15:41:31 -0700657 final int width = Math.min(adjMaxWidth, view.getMeasuredWidth());
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700658 final int left;
659 final int right;
660 if (mLayoutFromRight) {
Alan Viverettefb664152013-08-07 17:57:51 -0700661 right = (adjacent == null ? container.right : adjacent.getLeft()) - marginRight;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700662 left = right - width;
663 } else {
Alan Viverettefb664152013-08-07 17:57:51 -0700664 left = (adjacent == null ? container.left : adjacent.getRight()) + marginLeft;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700665 right = left + width;
666 }
Adam Powellaf5280c2011-10-11 18:36:34 -0700667
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700668 // 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);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800672 }
Alan Viverettee918a482013-06-07 11:43:06 -0700673
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700674 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
Alan Viverettefb664152013-08-07 17:57:51 -0700688 final Rect container = mContainerRect;
689 final int containerWidth = container.width();
690 final int adjMaxWidth = containerWidth - marginLeft - marginRight;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700691 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.
Alan Viverettefb664152013-08-07 17:57:51 -0700696 final int containerHeight = container.height();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700697 final int width = preview.getMeasuredWidth();
Alan Viverettefb664152013-08-07 17:57:51 -0700698 final int top = containerHeight / 10 + marginTop + container.top;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700699 final int bottom = top + preview.getMeasuredHeight();
Alan Viverettefb664152013-08-07 17:57:51 -0700700 final int left = (containerWidth - width) / 2 + container.left;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700701 final int right = left + width;
702 out.set(left, top, right, bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800703 }
Alan Viverettee918a482013-06-07 11:43:06 -0700704
Alan Viverette26bb2532013-08-09 10:40:50 -0700705 /**
706 * Updates the container rectangle used for layout.
707 */
Alan Viverettefb664152013-08-07 17:57:51 -0700708 private void updateContainerRect() {
709 final AbsListView list = mList;
Alan Viverette26bb2532013-08-09 10:40:50 -0700710 list.resolvePadding();
711
Alan Viverettefb664152013-08-07 17:57:51 -0700712 final Rect container = mContainerRect;
713 container.left = 0;
714 container.top = 0;
715 container.right = list.getWidth();
716 container.bottom = list.getHeight();
717
Alan Viverette26bb2532013-08-09 10:40:50 -0700718 final int scrollbarStyle = mScrollBarStyle;
Alan Viverettefb664152013-08-07 17:57:51 -0700719 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();
Alan Viverette26bb2532013-08-09 10:40:50 -0700725
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 }
Alan Viverettefb664152013-08-07 17:57:51 -0700735 }
736 }
737
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700738 /**
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);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800745 }
Alan Viverettee918a482013-06-07 11:43:06 -0700746
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700747 /**
Alan Viverettefb664152013-08-07 17:57:51 -0700748 * Lays out the track centered on the thumb. Must be called after
749 * {@link #layoutThumb}.
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700750 */
751 private void layoutTrack() {
752 final View track = mTrackImage;
753 final View thumb = mThumbImage;
Alan Viverettefb664152013-08-07 17:57:51 -0700754 final Rect container = mContainerRect;
755 final int containerWidth = container.width();
756 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(containerWidth, MeasureSpec.AT_MOST);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700757 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
758 track.measure(widthMeasureSpec, heightMeasureSpec);
Alan Viverettee918a482013-06-07 11:43:06 -0700759
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700760 final int trackWidth = track.getMeasuredWidth();
761 final int thumbHalfHeight = thumb == null ? 0 : thumb.getHeight() / 2;
Alan Viverettefb664152013-08-07 17:57:51 -0700762 final int left = thumb.getLeft() + (thumb.getWidth() - trackWidth) / 2;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700763 final int right = left + trackWidth;
Alan Viverettefb664152013-08-07 17:57:51 -0700764 final int top = container.top + thumbHalfHeight;
765 final int bottom = container.bottom - thumbHalfHeight;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700766 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) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800777 return;
778 }
779
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700780 switch (state) {
781 case STATE_NONE:
782 transitionToHidden();
783 break;
784 case STATE_VISIBLE:
785 transitionToVisible();
786 break;
787 case STATE_DRAGGING:
Alan Viverette6b40cc72013-06-25 16:41:52 -0700788 if (transitionPreviewLayout(mCurrentSection)) {
789 transitionToDragging();
790 } else {
791 transitionToVisible();
792 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700793 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800794 }
795
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700796 mState = state;
Adam Powell20232d02010-12-08 21:08:53 -0800797
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700798 refreshDrawablePressedState();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800799 }
800
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700801 private void refreshDrawablePressedState() {
802 final boolean isPressed = mState == STATE_DRAGGING;
803 mThumbImage.setPressed(isPressed);
804 mTrackImage.setPressed(isPressed);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800805 }
Adam Powell2c6196a2010-12-10 14:31:54 -0800806
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700807 /**
808 * Shows nothing.
809 */
810 private void transitionToHidden() {
811 if (mDecorAnimation != null) {
812 mDecorAnimation.cancel();
Adam Powell2c6196a2010-12-10 14:31:54 -0800813 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700814
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();
Alan Viverettef9af7b22013-10-15 17:09:35 -0700827
828 mShowingPreview = false;
Adam Powell2c6196a2010-12-10 14:31:54 -0800829 }
830
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700831 /**
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();
Alan Viverettef9af7b22013-10-15 17:09:35 -0700850
851 mShowingPreview = false;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700852 }
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();
Alan Viverettef9af7b22013-10-15 17:09:35 -0700871
872 mShowingPreview = true;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700873 }
874
Alan Viverettea709b372013-07-25 14:43:24 -0700875 private void postAutoHide() {
876 mList.removeCallbacks(mDeferHide);
877 mList.postDelayed(mDeferHide, FADE_TIMEOUT);
878 }
879
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700880 public void onScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) {
Alan Viveretteb9f27222013-09-06 19:39:47 -0700881 if (!isEnabled()) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700882 setState(STATE_NONE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800883 return;
884 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700885
886 final boolean hasMoreItems = totalItemCount - visibleItemCount > 0;
887 if (hasMoreItems && mState != STATE_DRAGGING) {
888 setThumbPos(getPosFromItemCount(firstVisibleItem, visibleItemCount, totalItemCount));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800889 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700890
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800891 mScrollCompleted = true;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700892
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);
Alan Viverettea709b372013-07-25 14:43:24 -0700899 postAutoHide();
Adam Powell20232d02010-12-08 21:08:53 -0800900 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800901 }
902 }
903
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700904 private void getSectionsFromIndexer() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800905 mSectionIndexer = null;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700906
907 Adapter adapter = mList.getAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800908 if (adapter instanceof HeaderViewListAdapter) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700909 mHeaderCount = ((HeaderViewListAdapter) adapter).getHeadersCount();
910 adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800911 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700912
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800913 if (adapter instanceof ExpandableListConnector) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700914 final ExpandableListAdapter expAdapter = ((ExpandableListConnector) adapter)
915 .getAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800916 if (expAdapter instanceof SectionIndexer) {
917 mSectionIndexer = (SectionIndexer) expAdapter;
Alan Viverette4f434c72013-12-13 11:01:21 -0800918 mListAdapter = adapter;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800919 mSections = mSectionIndexer.getSections();
920 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700921 } else if (adapter instanceof SectionIndexer) {
Alan Viverette4f434c72013-12-13 11:01:21 -0800922 mListAdapter = adapter;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700923 mSectionIndexer = (SectionIndexer) adapter;
924 mSections = mSectionIndexer.getSections();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800925 } else {
Alan Viverette4f434c72013-12-13 11:01:21 -0800926 mListAdapter = adapter;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700927 mSections = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800928 }
929 }
930
Adam Powellb1f498a2011-01-18 20:43:23 -0800931 public void onSectionsChanged() {
932 mListAdapter = null;
933 }
934
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700935 /**
936 * Scrolls to a specific position within the section
937 * @param position
938 */
939 private void scrollTo(float position) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800940 mScrollCompleted = false;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700941
942 final int count = mList.getCount();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800943 final Object[] sections = mSections;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700944 final int sectionCount = sections == null ? 0 : sections.length;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800945 int sectionIndex;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700946 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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800953 // 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;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700960 int prevIndex = targetIndex;
961 int prevSection = targetSection;
962 int nextSection = targetSection + 1;
963
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800964 // Assume the next section is unique
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700965 if (targetSection < sectionCount - 1) {
966 nextIndex = mSectionIndexer.getPositionForSection(targetSection + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800967 }
Alan Viverettee918a482013-06-07 11:43:06 -0700968
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800969 // Find the previous index if we're slicing the previous section
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700970 if (nextIndex == targetIndex) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800971 // Non-existent letter
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700972 while (targetSection > 0) {
973 targetSection--;
974 prevIndex = mSectionIndexer.getPositionForSection(targetSection);
975 if (prevIndex != targetIndex) {
976 prevSection = targetSection;
977 sectionIndex = targetSection;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700978 break;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700979 } else if (targetSection == 0) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700980 // When section reaches 0 here, sectionIndex must follow it.
981 // Assuming mSectionIndexer.getPositionForSection(0) == 0.
982 sectionIndex = 0;
983 break;
984 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800985 }
986 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700987
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800988 // Find the next index, in case the assumed next index is not
Alan Viverettee918a482013-06-07 11:43:06 -0700989 // unique. For instance, if there is no P, then request for P's
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800990 // position actually returns Q's. So we need to look ahead to make
Alan Viverettee918a482013-06-07 11:43:06 -0700991 // sure that there is really a Q at Q's position. If not, move
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800992 // further down...
993 int nextNextSection = nextSection + 1;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700994 while (nextNextSection < sectionCount &&
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800995 mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
996 nextNextSection++;
997 nextSection++;
998 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700999
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001000 // Compute the beginning and ending scroll range percentage of the
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001001 // 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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001009 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001010 targetIndex = prevIndex + (int) ((nextIndex - prevIndex) * (position - prevPosition)
1011 / (nextPosition - prevPosition));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001012 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001013
1014 // Clamp to valid positions.
1015 targetIndex = MathUtils.constrain(targetIndex, 0, count - 1);
Alan Viverettee918a482013-06-07 11:43:06 -07001016
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001017 if (mList instanceof ExpandableListView) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001018 final ExpandableListView expList = (ExpandableListView) mList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001019 expList.setSelectionFromTop(expList.getFlatListPosition(
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001020 ExpandableListView.getPackedPositionForGroup(targetIndex + mHeaderCount)),
1021 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001022 } else if (mList instanceof ListView) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001023 ((ListView) mList).setSelectionFromTop(targetIndex + mHeaderCount, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001024 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001025 mList.setSelection(targetIndex + mHeaderCount);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001026 }
1027 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001028 final int index = MathUtils.constrain((int) (position * count), 0, count - 1);
Adam Powell7ee1ff12011-03-09 16:35:13 -08001029
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001030 if (mList instanceof ExpandableListView) {
1031 ExpandableListView expList = (ExpandableListView) mList;
1032 expList.setSelectionFromTop(expList.getFlatListPosition(
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001033 ExpandableListView.getPackedPositionForGroup(index + mHeaderCount)), 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001034 } else if (mList instanceof ListView) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001035 ((ListView)mList).setSelectionFromTop(index + mHeaderCount, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001036 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001037 mList.setSelection(index + mHeaderCount);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001038 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001039
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001040 sectionIndex = -1;
1041 }
1042
Alan Viverette6b40cc72013-06-25 16:41:52 -07001043 if (mCurrentSection != sectionIndex) {
1044 mCurrentSection = sectionIndex;
1045
Alan Viverettef9af7b22013-10-15 17:09:35 -07001046 final boolean hasPreview = transitionPreviewLayout(sectionIndex);
1047 if (!mShowingPreview && hasPreview) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001048 transitionToDragging();
Alan Viverettef9af7b22013-10-15 17:09:35 -07001049 } else if (mShowingPreview && !hasPreview) {
Alan Viverette6b40cc72013-06-25 16:41:52 -07001050 transitionToVisible();
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001051 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001052 }
1053 }
1054
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001055 /**
Alan Viverette6b40cc72013-06-25 16:41:52 -07001056 * Transitions the preview text to a new section. Handles animation,
1057 * measurement, and layout. If the new preview text is empty, returns false.
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001058 *
Alan Viverette6b40cc72013-06-25 16:41:52 -07001059 * @param sectionIndex The section index to which the preview should
1060 * transition.
1061 * @return False if the new preview text is empty.
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001062 */
Alan Viverette6b40cc72013-06-25 16:41:52 -07001063 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
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001073 final Rect bounds = mTempBounds;
Alan Viverette8636ace2013-10-31 15:41:31 -07001074 final View preview = mPreviewImage;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001075 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.
Alan Viverette8636ace2013-10-31 15:41:31 -07001100 bounds.left -= preview.getPaddingLeft();
1101 bounds.top -= preview.getPaddingTop();
1102 bounds.right += preview.getPaddingRight();
1103 bounds.bottom += preview.getPaddingBottom();
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001104 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();
Alan Viverette6b40cc72013-06-25 16:41:52 -07001136
Alan Viverettef9af7b22013-10-15 17:09:35 -07001137 return !TextUtils.isEmpty(text);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001138 }
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) {
Alan Viverettefb664152013-08-07 17:57:51 -07001147 final Rect container = mContainerRect;
1148 final int top = container.top;
1149 final int bottom = container.bottom;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001150
Alan Viverette8636ace2013-10-31 15:41:31 -07001151 final View trackImage = mTrackImage;
1152 final View thumbImage = mThumbImage;
Alan Viverettefb664152013-08-07 17:57:51 -07001153 final float min = trackImage.getTop();
1154 final float max = trackImage.getBottom();
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001155 final float offset = min;
1156 final float range = max - min;
1157 final float thumbMiddle = position * range + offset;
Alan Viverettefb664152013-08-07 17:57:51 -07001158 thumbImage.setTranslationY(thumbMiddle - thumbImage.getHeight() / 2);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001159
Alan Viverette8636ace2013-10-31 15:41:31 -07001160 final View previewImage = mPreviewImage;
Alan Viverettefb664152013-08-07 17:57:51 -07001161 final float previewHalfHeight = previewImage.getHeight() / 2f;
Alan Viverettee8311ac2014-08-15 19:58:04 -07001162 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.
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001177 final float minP = top + previewHalfHeight;
1178 final float maxP = bottom - previewHalfHeight;
Adam Powell5db566f2013-10-13 17:19:10 -07001179 final float previewMiddle = MathUtils.constrain(previewPos, minP, maxP);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001180 final float previewTop = previewMiddle - previewHalfHeight;
Alan Viverettefb664152013-08-07 17:57:51 -07001181 previewImage.setTranslationY(previewTop);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001182
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001183 mPrimaryText.setTranslationY(previewTop);
1184 mSecondaryText.setTranslationY(previewTop);
1185 }
1186
1187 private float getPosFromMotionEvent(float y) {
Alan Viverette8636ace2013-10-31 15:41:31 -07001188 final View trackImage = mTrackImage;
Alan Viverettefb664152013-08-07 17:57:51 -07001189 final float min = trackImage.getTop();
1190 final float max = trackImage.getBottom();
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001191 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
Alan Viverette827015e2014-11-13 10:26:53 -08001203 /**
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 */
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001211 private float getPosFromItemCount(
1212 int firstVisibleItem, int visibleItemCount, int totalItemCount) {
Alan Viverette827015e2014-11-13 10:26:53 -08001213 final SectionIndexer sectionIndexer = mSectionIndexer;
1214 if (sectionIndexer == null || mListAdapter == null) {
Adam Powell32c3a692011-01-09 21:28:43 -08001215 getSectionsFromIndexer();
1216 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001217
Alan Viverette827015e2014-11-13 10:26:53 -08001218 if (visibleItemCount == 0 || totalItemCount == 0) {
1219 // No items are visible.
1220 return 0;
1221 }
1222
1223 final boolean hasSections = sectionIndexer != null && mSections != null
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001224 && mSections.length > 0;
1225 if (!hasSections || !mMatchDragPosition) {
Alan Viverette827015e2014-11-13 10:26:53 -08001226 if (visibleItemCount == totalItemCount) {
1227 // All items are visible.
1228 return 0;
1229 } else {
1230 return (float) firstVisibleItem / (totalItemCount - visibleItemCount);
1231 }
Adam Powell32c3a692011-01-09 21:28:43 -08001232 }
Alan Viverette827015e2014-11-13 10:26:53 -08001233
Alan Viverette6b40cc72013-06-25 16:41:52 -07001234 // Ignore headers.
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001235 firstVisibleItem -= mHeaderCount;
Adam Powell32c3a692011-01-09 21:28:43 -08001236 if (firstVisibleItem < 0) {
1237 return 0;
1238 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001239 totalItemCount -= mHeaderCount;
Adam Powell32c3a692011-01-09 21:28:43 -08001240
Alan Viverette6b40cc72013-06-25 16:41:52 -07001241 // 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.
Alan Viverette827015e2014-11-13 10:26:53 -08001251 final int section = sectionIndexer.getSectionForPosition(firstVisibleItem);
1252 final int sectionPos = sectionIndexer.getPositionForSection(section);
Adam Powellf49971e2011-06-14 22:00:01 -07001253 final int sectionCount = mSections.length;
Alan Viverette6b40cc72013-06-25 16:41:52 -07001254 final int positionsInSection;
1255 if (section < sectionCount - 1) {
Jean-Baptiste Queru414b0232013-07-08 15:02:41 -07001256 final int nextSectionPos;
1257 if (section + 1 < sectionCount) {
Alan Viverette827015e2014-11-13 10:26:53 -08001258 nextSectionPos = sectionIndexer.getPositionForSection(section + 1);
Jean-Baptiste Queru414b0232013-07-08 15:02:41 -07001259 } else {
1260 nextSectionPos = totalItemCount - 1;
1261 }
Alan Viverette6b40cc72013-06-25 16:41:52 -07001262 positionsInSection = nextSectionPos - sectionPos;
1263 } else {
1264 positionsInSection = totalItemCount - sectionPos;
1265 }
Adam Powell32c3a692011-01-09 21:28:43 -08001266
Alan Viverette6b40cc72013-06-25 16:41:52 -07001267 // 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
Alan Viverettef9af7b22013-10-15 17:09:35 -07001276 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);
Yigit Boyar40c6c552014-09-18 16:01:28 -07001283 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 }
Alan Viverettef9af7b22013-10-15 17:09:35 -07001296 }
1297
1298 return result;
Adam Powell32c3a692011-01-09 21:28:43 -08001299 }
1300
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001301 /**
1302 * Cancels an ongoing fling event by injecting a
1303 * {@link MotionEvent#ACTION_CANCEL} into the host view.
1304 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001305 private void cancelFling() {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001306 final MotionEvent cancelFling = MotionEvent.obtain(
1307 0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001308 mList.onTouchEvent(cancelFling);
1309 cancelFling.recycle();
1310 }
Alan Viverettee918a482013-06-07 11:43:06 -07001311
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001312 /**
1313 * Cancels a pending drag.
1314 *
1315 * @see #startPendingDrag()
1316 */
1317 private void cancelPendingDrag() {
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001318 mPendingDrag = -1;
Adam Powellaf5280c2011-10-11 18:36:34 -07001319 }
1320
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001321 /**
1322 * Delays dragging until after the framework has determined that the user is
1323 * scrolling, rather than tapping.
1324 */
1325 private void startPendingDrag() {
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001326 mPendingDrag = SystemClock.uptimeMillis() + TAP_TIMEOUT;
Adam Powellaf5280c2011-10-11 18:36:34 -07001327 }
1328
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001329 private void beginDrag() {
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001330 mPendingDrag = -1;
1331
Adam Powellaf5280c2011-10-11 18:36:34 -07001332 setState(STATE_DRAGGING);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001333
Adam Powellaf5280c2011-10-11 18:36:34 -07001334 if (mListAdapter == null && mList != null) {
1335 getSectionsFromIndexer();
1336 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001337
Adam Powellaf5280c2011-10-11 18:36:34 -07001338 if (mList != null) {
1339 mList.requestDisallowInterceptTouchEvent(true);
1340 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
1341 }
1342
1343 cancelFling();
1344 }
1345
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001346 public boolean onInterceptTouchEvent(MotionEvent ev) {
Alan Viveretteb9f27222013-09-06 19:39:47 -07001347 if (!isEnabled()) {
Alan Viverette447cdf22013-07-15 17:47:34 -07001348 return false;
1349 }
1350
Adam Powellaf5280c2011-10-11 18:36:34 -07001351 switch (ev.getActionMasked()) {
1352 case MotionEvent.ACTION_DOWN:
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001353 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.
Alan Viveretteb9f27222013-09-06 19:39:47 -07001359 if (!mList.isInScrollingContainer()) {
1360 beginDrag();
1361 return true;
Adam Powellaf5280c2011-10-11 18:36:34 -07001362 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001363
Alan Viveretteb9f27222013-09-06 19:39:47 -07001364 mInitialTouchY = ev.getY();
1365 startPendingDrag();
1366 }
1367 break;
1368 case MotionEvent.ACTION_MOVE:
1369 if (!isPointInside(ev.getX(), ev.getY())) {
1370 cancelPendingDrag();
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001371 } else if (mPendingDrag >= 0 && mPendingDrag <= SystemClock.uptimeMillis()) {
1372 beginDrag();
1373
1374 final float pos = getPosFromMotionEvent(mInitialTouchY);
1375 scrollTo(pos);
1376
1377 return onTouchEvent(ev);
Adam Powellaf5280c2011-10-11 18:36:34 -07001378 }
1379 break;
1380 case MotionEvent.ACTION_UP:
1381 case MotionEvent.ACTION_CANCEL:
1382 cancelPendingDrag();
1383 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001384 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001385
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001386 return false;
1387 }
1388
Alan Viverettea709b372013-07-25 14:43:24 -07001389 public boolean onInterceptHoverEvent(MotionEvent ev) {
Alan Viveretteb9f27222013-09-06 19:39:47 -07001390 if (!isEnabled()) {
Alan Viverettea709b372013-07-25 14:43:24 -07001391 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
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001405 public boolean onTouchEvent(MotionEvent me) {
Alan Viveretteb9f27222013-09-06 19:39:47 -07001406 if (!isEnabled()) {
Alan Viverette447cdf22013-07-15 17:47:34 -07001407 return false;
1408 }
1409
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001410 switch (me.getActionMasked()) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001411 case MotionEvent.ACTION_UP: {
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001412 if (mPendingDrag >= 0) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001413 // Allow a tap to scroll.
1414 beginDrag();
Adam Powellaf5280c2011-10-11 18:36:34 -07001415
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001416 final float pos = getPosFromMotionEvent(me.getY());
1417 setThumbPos(pos);
1418 scrollTo(pos);
1419
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001420 // Will hit the STATE_DRAGGING check below
Adam Powell20232d02010-12-08 21:08:53 -08001421 }
1422
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001423 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);
Alan Viverettea709b372013-07-25 14:43:24 -07001433 postAutoHide();
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001434
1435 return true;
1436 }
1437 } break;
1438
1439 case MotionEvent.ACTION_MOVE: {
Alan Viverette4cfeedb2013-12-03 14:03:24 -08001440 if (mPendingDrag >= 0 && Math.abs(me.getY() - mInitialTouchY) > mScaledTouchSlop) {
1441 beginDrag();
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001442
Adam Powellaf5280c2011-10-11 18:36:34 -07001443 // Will hit the STATE_DRAGGING check below
1444 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001445
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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001456 return true;
1457 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001458 } break;
1459
1460 case MotionEvent.ACTION_CANCEL: {
1461 cancelPendingDrag();
1462 } break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001463 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001464
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001465 return false;
1466 }
Romain Guyd6a463a2009-05-21 23:10:10 -07001467
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001468 /**
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) {
Alan Viverette8636ace2013-10-31 15:41:31 -07001479 return isPointInsideX(x) && (mTrackDrawable != null || isPointInsideY(y));
Romain Guy82f34952009-05-24 18:40:45 -07001480 }
1481
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001482 private boolean isPointInsideX(float x) {
Alan Viverette7d5bcd72014-11-20 16:07:59 -08001483 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
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001491 if (mLayoutFromRight) {
Alan Viverette7d5bcd72014-11-20 16:07:59 -08001492 return x >= mThumbImage.getLeft() - adjust;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001493 } else {
Alan Viverette7d5bcd72014-11-20 16:07:59 -08001494 return x <= mThumbImage.getRight() + adjust;
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001495 }
1496 }
Alan Viverettee918a482013-06-07 11:43:06 -07001497
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001498 private boolean isPointInsideY(float y) {
Adam Powelld8273892013-10-13 13:35:27 -07001499 final float offset = mThumbImage.getTranslationY();
1500 final float top = mThumbImage.getTop() + offset;
1501 final float bottom = mThumbImage.getBottom() + offset;
Alan Viverette7d5bcd72014-11-20 16:07:59 -08001502
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);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001508 }
Alan Viverettee918a482013-06-07 11:43:06 -07001509
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001510 /**
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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001532 }
Alan Viverettee918a482013-06-07 11:43:06 -07001533
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001534 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);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001559 }
Alan Viverettee918a482013-06-07 11:43:06 -07001560
1561 @Override
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001562 public Integer get(View object) {
1563 return object.getLeft();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001564 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001565 };
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);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001624 }
1625}