blob: f3c06876279ac0f493effc7838440eaf2443359a [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;
Alan Viverettee918a482013-06-07 11:43:06 -070027import android.content.res.Resources;
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;
32import android.text.TextUtils.TruncateAt;
33import android.util.IntProperty;
34import android.util.MathUtils;
35import android.util.Property;
36import android.view.Gravity;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080037import android.view.MotionEvent;
Adam Powell20232d02010-12-08 21:08:53 -080038import android.view.View;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070039import android.view.View.MeasureSpec;
Adam Powellaf5280c2011-10-11 18:36:34 -070040import android.view.ViewConfiguration;
Alan Viverette0ebe81e2013-06-21 17:01:36 -070041import android.view.ViewGroup.LayoutParams;
42import android.view.ViewGroupOverlay;
Adam Powelld43bd482010-02-26 16:29:09 -080043import android.widget.AbsListView.OnScrollListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080044
Alan Viverette0ebe81e2013-06-21 17:01:36 -070045import com.android.internal.R;
46
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080047/**
48 * Helper class for AbsListView to draw and control the Fast Scroll thumb
49 */
50class FastScroller {
Alan Viverette0ebe81e2013-06-21 17:01:36 -070051 /** Duration of fade-out animation. */
52 private static final int DURATION_FADE_OUT = 300;
Alan Viverettee918a482013-06-07 11:43:06 -070053
Alan Viverette0ebe81e2013-06-21 17:01:36 -070054 /** Duration of fade-in animation. */
55 private static final int DURATION_FADE_IN = 150;
56
57 /** Duration of transition cross-fade animation. */
58 private static final int DURATION_CROSS_FADE = 50;
59
60 /** Duration of transition resize animation. */
61 private static final int DURATION_RESIZE = 100;
62
63 /** Inactivity timeout before fading controls. */
64 private static final long FADE_TIMEOUT = 1500;
65
66 /** Minimum number of pages to justify showing a fast scroll thumb. */
67 private static final int MIN_PAGES = 4;
68
69 /** Scroll thumb and preview not showing. */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080070 private static final int STATE_NONE = 0;
Adam Powell20232d02010-12-08 21:08:53 -080071
Alan Viverette0ebe81e2013-06-21 17:01:36 -070072 /** Scroll thumb visible and moving along with the scrollbar. */
73 private static final int STATE_VISIBLE = 1;
Adam Powell20232d02010-12-08 21:08:53 -080074
Alan Viverette0ebe81e2013-06-21 17:01:36 -070075 /** Scroll thumb and preview being dragged by user. */
76 private static final int STATE_DRAGGING = 2;
Adam Powell20232d02010-12-08 21:08:53 -080077
Alan Viverette0ebe81e2013-06-21 17:01:36 -070078 /** Styleable attributes. */
Adam Powell20232d02010-12-08 21:08:53 -080079 private static final int[] ATTRS = new int[] {
Adam Powellb2e55172011-01-15 17:21:35 -080080 android.R.attr.fastScrollTextColor,
Adam Powell128b6ba2010-12-13 12:33:44 -080081 android.R.attr.fastScrollThumbDrawable,
82 android.R.attr.fastScrollTrackDrawable,
83 android.R.attr.fastScrollPreviewBackgroundLeft,
84 android.R.attr.fastScrollPreviewBackgroundRight,
85 android.R.attr.fastScrollOverlayPosition
Adam Powell20232d02010-12-08 21:08:53 -080086 };
87
Alan Viverette0ebe81e2013-06-21 17:01:36 -070088 // Styleable attribute indices.
Adam Powellb2e55172011-01-15 17:21:35 -080089 private static final int TEXT_COLOR = 0;
Adam Powell20232d02010-12-08 21:08:53 -080090 private static final int THUMB_DRAWABLE = 1;
91 private static final int TRACK_DRAWABLE = 2;
92 private static final int PREVIEW_BACKGROUND_LEFT = 3;
93 private static final int PREVIEW_BACKGROUND_RIGHT = 4;
94 private static final int OVERLAY_POSITION = 5;
95
Alan Viverette0ebe81e2013-06-21 17:01:36 -070096 // Positions for preview image and text.
Adam Powell20232d02010-12-08 21:08:53 -080097 private static final int OVERLAY_FLOATING = 0;
98 private static final int OVERLAY_AT_THUMB = 1;
Alan Viverettee918a482013-06-07 11:43:06 -070099
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700100 // Indices for mPreviewResId.
101 private static final int PREVIEW_LEFT = 0;
102 private static final int PREVIEW_RIGHT = 1;
Adam Powell20232d02010-12-08 21:08:53 -0800103
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700104 /** Delay before considering a tap in the thumb area to be a drag. */
105 private static final long TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800106
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700107 private final Rect mTempBounds = new Rect();
108 private final Rect mTempMargins = new Rect();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800109
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700110 private final AbsListView mList;
111 private final ViewGroupOverlay mOverlay;
112 private final TextView mPrimaryText;
113 private final TextView mSecondaryText;
114 private final ImageView mThumbImage;
115 private final ImageView mTrackImage;
116 private final ImageView mPreviewImage;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800117
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700118 /**
119 * Preview image resource IDs for left- and right-aligned layouts. See
120 * {@link #PREVIEW_LEFT} and {@link #PREVIEW_RIGHT}.
121 */
122 private final int[] mPreviewResId = new int[2];
123
124 /**
125 * Padding in pixels around the preview text. Applied as layout margins to
126 * the preview text and padding to the preview image.
127 */
128 private final int mPreviewPadding;
129
130 /** Whether there is a track image to display. */
131 private final boolean mHasTrackImage;
132
133 /** Set containing decoration transition animations. */
134 private AnimatorSet mDecorAnimation;
135
136 /** Set containing preview text transition animations. */
137 private AnimatorSet mPreviewAnimation;
138
139 /** Whether the primary text is showing. */
140 private boolean mShowingPrimary;
141
142 /** Whether we're waiting for completion of scrollTo(). */
143 private boolean mScrollCompleted;
144
145 /** The position of the first visible item in the list. */
146 private int mFirstVisibleItem;
147
148 /** The number of headers at the top of the view. */
149 private int mHeaderCount;
150
151 /** The number of items in the list. */
The Android Open Source Project4df24232009-03-05 14:34:35 -0800152 private int mItemCount = -1;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700153
154 /** The index of the current section. */
155 private int mCurrentSection = -1;
156
157 /** Whether the list is long enough to need a fast scroller. */
The Android Open Source Project4df24232009-03-05 14:34:35 -0800158 private boolean mLongList;
Alan Viverettee918a482013-06-07 11:43:06 -0700159
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700160 private Object[] mSections;
Alan Viverettee918a482013-06-07 11:43:06 -0700161
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700162 /**
163 * Current decoration state, one of:
164 * <ul>
165 * <li>{@link #STATE_NONE}, nothing visible
166 * <li>{@link #STATE_VISIBLE}, showing track and thumb
167 * <li>{@link #STATE_DRAGGING}, visible and showing preview
168 * </ul>
169 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800170 private int mState;
Alan Viverettee918a482013-06-07 11:43:06 -0700171
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700172 private BaseAdapter mListAdapter;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800173 private SectionIndexer mSectionIndexer;
174
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700175 /** Whether decorations should be laid out from right to left. */
176 private boolean mLayoutFromRight;
Alan Viverettee918a482013-06-07 11:43:06 -0700177
Alan Viverette447cdf22013-07-15 17:47:34 -0700178 /** Whether the fast scroller is enabled. */
179 private boolean mEnabled;
180
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700181 /** Whether the scrollbar and decorations should always be shown. */
Adam Powell20232d02010-12-08 21:08:53 -0800182 private boolean mAlwaysShow;
183
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700184 /**
185 * Position for the preview image and text. One of:
186 * <ul>
187 * <li>{@link #OVERLAY_AT_THUMB}
188 * <li>{@link #OVERLAY_FLOATING}
189 * </ul>
190 */
Adam Powell20232d02010-12-08 21:08:53 -0800191 private int mOverlayPosition;
192
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700193 /** Whether to precisely match the thumb position to the list. */
Adam Powell568ccd82011-08-03 22:38:48 -0700194 private boolean mMatchDragPosition;
195
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700196 private float mInitialTouchY;
197 private boolean mHasPendingDrag;
Adam Powellaf5280c2011-10-11 18:36:34 -0700198 private int mScaledTouchSlop;
199
Adam Powellaf5280c2011-10-11 18:36:34 -0700200 private final Runnable mDeferStartDrag = new Runnable() {
Alan Viverettee918a482013-06-07 11:43:06 -0700201 @Override
Adam Powellaf5280c2011-10-11 18:36:34 -0700202 public void run() {
203 if (mList.mIsAttached) {
204 beginDrag();
205
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700206 final float pos = getPosFromMotionEvent(mInitialTouchY);
207 scrollTo(pos);
Adam Powellaf5280c2011-10-11 18:36:34 -0700208 }
209
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700210 mHasPendingDrag = false;
211 }
212 };
213
214 /**
215 * Used to delay hiding fast scroll decorations.
216 */
217 private final Runnable mDeferHide = new Runnable() {
218 @Override
219 public void run() {
220 setState(STATE_NONE);
221 }
222 };
223
224 /**
225 * Used to effect a transition from primary to secondary text.
226 */
227 private final AnimatorListener mSwitchPrimaryListener = new AnimatorListenerAdapter() {
228 @Override
229 public void onAnimationEnd(Animator animation) {
230 mShowingPrimary = !mShowingPrimary;
Adam Powellaf5280c2011-10-11 18:36:34 -0700231 }
232 };
233
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800234 public FastScroller(Context context, AbsListView listView) {
235 mList = listView;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700236 mOverlay = listView.getOverlay();
237
238 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
239
240 final Resources res = context.getResources();
241 final TypedArray ta = context.getTheme().obtainStyledAttributes(ATTRS);
242
243 mTrackImage = new ImageView(context);
244
245 // Add track to overlay if it has an image.
246 final int trackResId = ta.getResourceId(TRACK_DRAWABLE, 0);
247 if (trackResId != 0) {
248 mHasTrackImage = true;
249 mTrackImage.setBackgroundResource(trackResId);
250 mOverlay.add(mTrackImage);
251 } else {
252 mHasTrackImage = false;
253 }
254
255 mThumbImage = new ImageView(context);
256
257 // Add thumb to overlay if it has an image.
258 final Drawable thumbDrawable = ta.getDrawable(THUMB_DRAWABLE);
259 if (thumbDrawable != null) {
260 mThumbImage.setImageDrawable(thumbDrawable);
261 mOverlay.add(mThumbImage);
262 }
263
264 // If necessary, apply minimum thumb width and height.
265 if (thumbDrawable.getIntrinsicWidth() <= 0 || thumbDrawable.getIntrinsicHeight() <= 0) {
266 mThumbImage.setMinimumWidth(res.getDimensionPixelSize(R.dimen.fastscroll_thumb_width));
267 mThumbImage.setMinimumHeight(
268 res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height));
269 }
270
271 final int previewSize = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_size);
272 mPreviewImage = new ImageView(context);
273 mPreviewImage.setMinimumWidth(previewSize);
274 mPreviewImage.setMinimumHeight(previewSize);
275 mPreviewImage.setAlpha(0f);
276 mOverlay.add(mPreviewImage);
277
278 mPreviewPadding = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_padding);
279
Alan Viverette6b40cc72013-06-25 16:41:52 -0700280 final int textMinSize = Math.max(0, previewSize - mPreviewPadding);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700281 mPrimaryText = createPreviewTextView(context, ta);
Alan Viverette6b40cc72013-06-25 16:41:52 -0700282 mPrimaryText.setMinimumWidth(textMinSize);
283 mPrimaryText.setMinimumHeight(textMinSize);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700284 mOverlay.add(mPrimaryText);
285 mSecondaryText = createPreviewTextView(context, ta);
Alan Viverette6b40cc72013-06-25 16:41:52 -0700286 mSecondaryText.setMinimumWidth(textMinSize);
287 mSecondaryText.setMinimumHeight(textMinSize);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700288 mOverlay.add(mSecondaryText);
289
290 mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(PREVIEW_BACKGROUND_LEFT, 0);
291 mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(PREVIEW_BACKGROUND_RIGHT, 0);
292 mOverlayPosition = ta.getInt(OVERLAY_POSITION, OVERLAY_FLOATING);
293 ta.recycle();
294
295 mScrollCompleted = true;
296 mState = STATE_VISIBLE;
297 mMatchDragPosition =
298 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB;
299
300 getSectionsFromIndexer();
301 refreshDrawablePressedState();
302 setScrollbarPosition(mList.getVerticalScrollbarPosition());
303
304 mList.postDelayed(mDeferHide, FADE_TIMEOUT);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800305 }
306
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700307 /**
Alan Viverette447cdf22013-07-15 17:47:34 -0700308 * Removes this FastScroller overlay from the host view.
309 */
310 public void remove() {
311 mOverlay.remove(mTrackImage);
312 mOverlay.remove(mThumbImage);
313 mOverlay.remove(mPreviewImage);
314 mOverlay.remove(mPrimaryText);
315 mOverlay.remove(mSecondaryText);
316 }
317
318 /**
319 * @param enabled Whether the fast scroll thumb is enabled.
320 */
321 public void setEnabled(boolean enabled) {
322 mEnabled = enabled;
323
324 if (enabled) {
325 if (mAlwaysShow) {
326 setState(STATE_VISIBLE);
327 }
328 } else {
329 stop();
330 }
331 }
332
333 /**
334 * @return Whether the fast scroll thumb is enabled.
335 */
336 public boolean isEnabled() {
337 return mEnabled;
338 }
339
340 /**
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700341 * @param alwaysShow Whether the fast scroll thumb should always be shown
342 */
Adam Powell20232d02010-12-08 21:08:53 -0800343 public void setAlwaysShow(boolean alwaysShow) {
344 mAlwaysShow = alwaysShow;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700345
Adam Powell20232d02010-12-08 21:08:53 -0800346 if (alwaysShow) {
Adam Powell20232d02010-12-08 21:08:53 -0800347 setState(STATE_VISIBLE);
348 } else if (mState == STATE_VISIBLE) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700349 mList.postDelayed(mDeferHide, FADE_TIMEOUT);
Adam Powell20232d02010-12-08 21:08:53 -0800350 }
351 }
352
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700353 /**
354 * @return Whether the fast scroll thumb will always be shown
355 * @see #setAlwaysShow(boolean)
356 */
Adam Powell20232d02010-12-08 21:08:53 -0800357 public boolean isAlwaysShowEnabled() {
358 return mAlwaysShow;
359 }
360
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700361 /**
362 * Immediately transitions the fast scroller decorations to a hidden state.
363 */
364 public void stop() {
365 setState(STATE_NONE);
366 }
Adam Powell20232d02010-12-08 21:08:53 -0800367
Adam Powell20232d02010-12-08 21:08:53 -0800368 public void setScrollbarPosition(int position) {
Fabrice Di Meglioc23ee462012-06-22 18:46:06 -0700369 if (position == View.SCROLLBAR_POSITION_DEFAULT) {
370 position = mList.isLayoutRtl() ?
371 View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT;
372 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700373
374 mLayoutFromRight = position != View.SCROLLBAR_POSITION_LEFT;
375
376 final int previewResId = mPreviewResId[mLayoutFromRight ? PREVIEW_RIGHT : PREVIEW_LEFT];
377 mPreviewImage.setBackgroundResource(previewResId);
378
379 // Add extra padding for text.
380 final Drawable background = mPreviewImage.getBackground();
381 if (background != null) {
382 final Rect padding = mTempBounds;
383 background.getPadding(padding);
384 padding.offset(mPreviewPadding, mPreviewPadding);
385 mPreviewImage.setPadding(padding.left, padding.top, padding.right, padding.bottom);
Adam Powell20232d02010-12-08 21:08:53 -0800386 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700387
388 updateLayout();
Adam Powell20232d02010-12-08 21:08:53 -0800389 }
390
391 public int getWidth() {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700392 return mThumbImage.getWidth();
Adam Powell20232d02010-12-08 21:08:53 -0800393 }
394
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700395 public void onSizeChanged(int w, int h, int oldw, int oldh) {
396 updateLayout();
397 }
398
399 public void onItemCountChanged(int oldTotalItemCount, int totalItemCount) {
400 final int visibleItemCount = mList.getChildCount();
401 final boolean hasMoreItems = totalItemCount - visibleItemCount > 0;
402 if (hasMoreItems && mState != STATE_DRAGGING) {
403 final int firstVisibleItem = mList.getFirstVisiblePosition();
404 setThumbPos(getPosFromItemCount(firstVisibleItem, visibleItemCount, totalItemCount));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800405 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800406 }
Alan Viverettee918a482013-06-07 11:43:06 -0700407
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700408 /**
409 * Creates a view into which preview text can be placed.
410 */
411 private TextView createPreviewTextView(Context context, TypedArray ta) {
412 final LayoutParams params = new LayoutParams(
413 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
Alan Viverettee918a482013-06-07 11:43:06 -0700414 final Resources res = context.getResources();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700415 final int minSize = res.getDimensionPixelSize(R.dimen.fastscroll_overlay_size);
416 final ColorStateList textColor = ta.getColorStateList(TEXT_COLOR);
417 final float textSize = res.getDimension(R.dimen.fastscroll_overlay_text_size);
418 final TextView textView = new TextView(context);
419 textView.setLayoutParams(params);
420 textView.setTextColor(textColor);
421 textView.setTextSize(textSize);
422 textView.setSingleLine(true);
423 textView.setEllipsize(TruncateAt.MIDDLE);
424 textView.setGravity(Gravity.CENTER);
425 textView.setAlpha(0f);
Adam Powell20232d02010-12-08 21:08:53 -0800426
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700427 // Manually propagate inherited layout direction.
428 textView.setLayoutDirection(mList.getLayoutDirection());
NoraBora9b38c602010-10-12 06:59:55 -0700429
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700430 return textView;
431 }
432
433 /**
434 * Measures and layouts the scrollbar and decorations.
435 */
436 private void updateLayout() {
437 layoutThumb();
438 layoutTrack();
439
440 final Rect bounds = mTempBounds;
441 measurePreview(mPrimaryText, bounds);
442 applyLayout(mPrimaryText, bounds);
443 measurePreview(mSecondaryText, bounds);
444 applyLayout(mSecondaryText, bounds);
445
446 if (mPreviewImage != null) {
447 // Apply preview image padding.
448 bounds.left -= mPreviewImage.getPaddingLeft();
449 bounds.top -= mPreviewImage.getPaddingTop();
450 bounds.right += mPreviewImage.getPaddingRight();
451 bounds.bottom += mPreviewImage.getPaddingBottom();
452 applyLayout(mPreviewImage, bounds);
453 }
454 }
455
456 /**
457 * Layouts a view within the specified bounds and pins the pivot point to
458 * the appropriate edge.
459 *
460 * @param view The view to layout.
461 * @param bounds Bounds at which to layout the view.
462 */
463 private void applyLayout(View view, Rect bounds) {
464 view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom);
465 view.setPivotX(mLayoutFromRight ? bounds.right - bounds.left : 0);
466 }
467
468 /**
469 * Measures the preview text bounds, taking preview image padding into
470 * account. This method should only be called after {@link #layoutThumb()}
471 * and {@link #layoutTrack()} have both been called at least once.
472 *
473 * @param v The preview text view to measure.
474 * @param out Rectangle into which measured bounds are placed.
475 */
476 private void measurePreview(View v, Rect out) {
477 // Apply the preview image's padding as layout margins.
478 final Rect margins = mTempMargins;
479 margins.left = mPreviewImage.getPaddingLeft();
480 margins.top = mPreviewImage.getPaddingTop();
481 margins.right = mPreviewImage.getPaddingRight();
482 margins.bottom = mPreviewImage.getPaddingBottom();
483
484 if (mOverlayPosition == OVERLAY_AT_THUMB) {
485 measureViewToSide(v, mThumbImage, margins, out);
486 } else {
487 measureFloating(v, margins, out);
488 }
489 }
490
491 /**
492 * Measures the bounds for a view that should be laid out against the edge
493 * of an adjacent view. If no adjacent view is provided, lays out against
494 * the list edge.
495 *
496 * @param view The view to measure for layout.
497 * @param adjacent (Optional) The adjacent view, may be null to align to the
498 * list edge.
499 * @param margins Layout margins to apply to the view.
500 * @param out Rectangle into which measured bounds are placed.
501 */
502 private void measureViewToSide(View view, View adjacent, Rect margins, Rect out) {
503 final int marginLeft;
504 final int marginTop;
505 final int marginRight;
506 if (margins == null) {
507 marginLeft = 0;
508 marginTop = 0;
509 marginRight = 0;
510 } else {
511 marginLeft = margins.left;
512 marginTop = margins.top;
513 marginRight = margins.right;
NoraBora9b38c602010-10-12 06:59:55 -0700514 }
Alan Viverettee918a482013-06-07 11:43:06 -0700515
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700516 final int listWidth = mList.getWidth();
517 final int maxWidth;
518 if (adjacent == null) {
519 maxWidth = listWidth;
520 } else if (mLayoutFromRight) {
521 maxWidth = adjacent.getLeft();
522 } else {
523 maxWidth = listWidth - adjacent.getRight();
524 }
Adam Powell20232d02010-12-08 21:08:53 -0800525
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700526 final int adjMaxWidth = maxWidth - marginLeft - marginRight;
527 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
528 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
529 view.measure(widthMeasureSpec, heightMeasureSpec);
Adam Powell20232d02010-12-08 21:08:53 -0800530
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700531 // Align to the left or right.
532 final int width = view.getMeasuredWidth();
533 final int left;
534 final int right;
535 if (mLayoutFromRight) {
536 right = (adjacent == null ? listWidth : adjacent.getLeft()) - marginRight;
537 left = right - width;
538 } else {
539 left = (adjacent == null ? 0 : adjacent.getRight()) + marginLeft;
540 right = left + width;
541 }
Adam Powellaf5280c2011-10-11 18:36:34 -0700542
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700543 // Don't adjust the vertical position.
544 final int top = marginTop;
545 final int bottom = top + view.getMeasuredHeight();
546 out.set(left, top, right, bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800547 }
Alan Viverettee918a482013-06-07 11:43:06 -0700548
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700549 private void measureFloating(View preview, Rect margins, Rect out) {
550 final int marginLeft;
551 final int marginTop;
552 final int marginRight;
553 if (margins == null) {
554 marginLeft = 0;
555 marginTop = 0;
556 marginRight = 0;
557 } else {
558 marginLeft = margins.left;
559 marginTop = margins.top;
560 marginRight = margins.right;
561 }
562
563 final View list = mList;
564 final int listWidth = list.getWidth();
565 final int adjMaxWidth = listWidth - marginLeft - marginRight;
566 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
567 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
568 preview.measure(widthMeasureSpec, heightMeasureSpec);
569
570 // Align at the vertical center, 10% from the top.
571 final int width = preview.getMeasuredWidth();
572 final int top = list.getHeight() / 10 + marginTop;
573 final int bottom = top + preview.getMeasuredHeight();
574 final int left = (listWidth - width) / 2;
575 final int right = left + width;
576 out.set(left, top, right, bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800577 }
Alan Viverettee918a482013-06-07 11:43:06 -0700578
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700579 /**
580 * Lays out the thumb according to the current scrollbar position.
581 */
582 private void layoutThumb() {
583 final Rect bounds = mTempBounds;
584 measureViewToSide(mThumbImage, null, null, bounds);
585 applyLayout(mThumbImage, bounds);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800586 }
Alan Viverettee918a482013-06-07 11:43:06 -0700587
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700588 /**
589 * Lays out the track centered on the thumb, if available, or against the
590 * edge if no thumb is available. Must be called after {@link #layoutThumb}.
591 */
592 private void layoutTrack() {
593 final View track = mTrackImage;
594 final View thumb = mThumbImage;
595 final View list = mList;
596 final int listWidth = list.getWidth();
597 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(listWidth, MeasureSpec.AT_MOST);
598 final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
599 track.measure(widthMeasureSpec, heightMeasureSpec);
Alan Viverettee918a482013-06-07 11:43:06 -0700600
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700601 final int trackWidth = track.getMeasuredWidth();
602 final int thumbHalfHeight = thumb == null ? 0 : thumb.getHeight() / 2;
603 final int left = thumb == null ? listWidth - trackWidth :
604 thumb.getLeft() + (thumb.getWidth() - trackWidth) / 2;
605 final int right = left + trackWidth;
606 final int top = thumbHalfHeight;
607 final int bottom = list.getHeight() - thumbHalfHeight;
608 track.layout(left, top, right, bottom);
609 }
610
611 private void setState(int state) {
612 mList.removeCallbacks(mDeferHide);
613
614 if (mAlwaysShow && state == STATE_NONE) {
615 state = STATE_VISIBLE;
616 }
617
618 if (state == mState) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800619 return;
620 }
621
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700622 switch (state) {
623 case STATE_NONE:
624 transitionToHidden();
625 break;
626 case STATE_VISIBLE:
627 transitionToVisible();
628 break;
629 case STATE_DRAGGING:
Alan Viverette6b40cc72013-06-25 16:41:52 -0700630 if (transitionPreviewLayout(mCurrentSection)) {
631 transitionToDragging();
632 } else {
633 transitionToVisible();
634 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700635 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800636 }
637
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700638 mState = state;
Adam Powell20232d02010-12-08 21:08:53 -0800639
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700640 refreshDrawablePressedState();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800641 }
642
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700643 private void refreshDrawablePressedState() {
644 final boolean isPressed = mState == STATE_DRAGGING;
645 mThumbImage.setPressed(isPressed);
646 mTrackImage.setPressed(isPressed);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800647 }
Adam Powell2c6196a2010-12-10 14:31:54 -0800648
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700649 /**
650 * Shows nothing.
651 */
652 private void transitionToHidden() {
653 if (mDecorAnimation != null) {
654 mDecorAnimation.cancel();
Adam Powell2c6196a2010-12-10 14:31:54 -0800655 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700656
657 final Animator fadeOut = groupAnimatorOfFloat(View.ALPHA, 0f, mThumbImage, mTrackImage,
658 mPreviewImage, mPrimaryText, mSecondaryText).setDuration(DURATION_FADE_OUT);
659
660 // Push the thumb and track outside the list bounds.
661 final float offset = mLayoutFromRight ? mThumbImage.getWidth() : -mThumbImage.getWidth();
662 final Animator slideOut = groupAnimatorOfFloat(
663 View.TRANSLATION_X, offset, mThumbImage, mTrackImage)
664 .setDuration(DURATION_FADE_OUT);
665
666 mDecorAnimation = new AnimatorSet();
667 mDecorAnimation.playTogether(fadeOut, slideOut);
668 mDecorAnimation.start();
Adam Powell2c6196a2010-12-10 14:31:54 -0800669 }
670
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700671 /**
672 * Shows the thumb and track.
673 */
674 private void transitionToVisible() {
675 if (mDecorAnimation != null) {
676 mDecorAnimation.cancel();
677 }
678
679 final Animator fadeIn = groupAnimatorOfFloat(View.ALPHA, 1f, mThumbImage, mTrackImage)
680 .setDuration(DURATION_FADE_IN);
681 final Animator fadeOut = groupAnimatorOfFloat(
682 View.ALPHA, 0f, mPreviewImage, mPrimaryText, mSecondaryText)
683 .setDuration(DURATION_FADE_OUT);
684 final Animator slideIn = groupAnimatorOfFloat(
685 View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN);
686
687 mDecorAnimation = new AnimatorSet();
688 mDecorAnimation.playTogether(fadeIn, fadeOut, slideIn);
689 mDecorAnimation.start();
690 }
691
692 /**
693 * Shows the thumb, preview, and track.
694 */
695 private void transitionToDragging() {
696 if (mDecorAnimation != null) {
697 mDecorAnimation.cancel();
698 }
699
700 final Animator fadeIn = groupAnimatorOfFloat(
701 View.ALPHA, 1f, mThumbImage, mTrackImage, mPreviewImage)
702 .setDuration(DURATION_FADE_IN);
703 final Animator slideIn = groupAnimatorOfFloat(
704 View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN);
705
706 mDecorAnimation = new AnimatorSet();
707 mDecorAnimation.playTogether(fadeIn, slideIn);
708 mDecorAnimation.start();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700709 }
710
711 private boolean isLongList(int visibleItemCount, int totalItemCount) {
712 // Are there enough pages to require fast scroll? Recompute only if
713 // total count changes.
The Android Open Source Project4df24232009-03-05 14:34:35 -0800714 if (mItemCount != totalItemCount && visibleItemCount > 0) {
715 mItemCount = totalItemCount;
Adam Powell32c3a692011-01-09 21:28:43 -0800716 mLongList = mItemCount / visibleItemCount >= MIN_PAGES;
717 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700718
719 return mLongList;
720 }
721
722 public void onScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) {
Alan Viverette447cdf22013-07-15 17:47:34 -0700723 if (!mEnabled || !mAlwaysShow && !isLongList(visibleItemCount, totalItemCount)) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700724 setState(STATE_NONE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800725 return;
726 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700727
728 final boolean hasMoreItems = totalItemCount - visibleItemCount > 0;
729 if (hasMoreItems && mState != STATE_DRAGGING) {
730 setThumbPos(getPosFromItemCount(firstVisibleItem, visibleItemCount, totalItemCount));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800731 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700732
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800733 mScrollCompleted = true;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700734
735 if (mFirstVisibleItem != firstVisibleItem) {
736 mFirstVisibleItem = firstVisibleItem;
737
738 // Show the thumb, if necessary, and set up auto-fade.
739 if (mState != STATE_DRAGGING) {
740 setState(STATE_VISIBLE);
741 mList.postDelayed(mDeferHide, FADE_TIMEOUT);
Adam Powell20232d02010-12-08 21:08:53 -0800742 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800743 }
744 }
745
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700746 private void getSectionsFromIndexer() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800747 mSectionIndexer = null;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700748
749 Adapter adapter = mList.getAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800750 if (adapter instanceof HeaderViewListAdapter) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700751 mHeaderCount = ((HeaderViewListAdapter) adapter).getHeadersCount();
752 adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800753 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700754
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800755 if (adapter instanceof ExpandableListConnector) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700756 final ExpandableListAdapter expAdapter = ((ExpandableListConnector) adapter)
757 .getAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800758 if (expAdapter instanceof SectionIndexer) {
759 mSectionIndexer = (SectionIndexer) expAdapter;
760 mListAdapter = (BaseAdapter) adapter;
761 mSections = mSectionIndexer.getSections();
762 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700763 } else if (adapter instanceof SectionIndexer) {
764 mListAdapter = (BaseAdapter) adapter;
765 mSectionIndexer = (SectionIndexer) adapter;
766 mSections = mSectionIndexer.getSections();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800767 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700768 mListAdapter = (BaseAdapter) adapter;
769 mSections = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800770 }
771 }
772
Adam Powellb1f498a2011-01-18 20:43:23 -0800773 public void onSectionsChanged() {
774 mListAdapter = null;
775 }
776
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700777 /**
778 * Scrolls to a specific position within the section
779 * @param position
780 */
781 private void scrollTo(float position) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800782 mScrollCompleted = false;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700783
784 final int count = mList.getCount();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800785 final Object[] sections = mSections;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700786 final int sectionCount = sections == null ? 0 : sections.length;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800787 int sectionIndex;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700788 if (sections != null && sectionCount > 1) {
789 final int exactSection = MathUtils.constrain(
790 (int) (position * sectionCount), 0, sectionCount - 1);
791 int targetSection = exactSection;
792 int targetIndex = mSectionIndexer.getPositionForSection(targetSection);
793 sectionIndex = targetSection;
794
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800795 // Given the expected section and index, the following code will
796 // try to account for missing sections (no names starting with..)
797 // It will compute the scroll space of surrounding empty sections
798 // and interpolate the currently visible letter's range across the
799 // available space, so that there is always some list movement while
800 // the user moves the thumb.
801 int nextIndex = count;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700802 int prevIndex = targetIndex;
803 int prevSection = targetSection;
804 int nextSection = targetSection + 1;
805
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800806 // Assume the next section is unique
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700807 if (targetSection < sectionCount - 1) {
808 nextIndex = mSectionIndexer.getPositionForSection(targetSection + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800809 }
Alan Viverettee918a482013-06-07 11:43:06 -0700810
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800811 // Find the previous index if we're slicing the previous section
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700812 if (nextIndex == targetIndex) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800813 // Non-existent letter
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700814 while (targetSection > 0) {
815 targetSection--;
816 prevIndex = mSectionIndexer.getPositionForSection(targetSection);
817 if (prevIndex != targetIndex) {
818 prevSection = targetSection;
819 sectionIndex = targetSection;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700820 break;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700821 } else if (targetSection == 0) {
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700822 // When section reaches 0 here, sectionIndex must follow it.
823 // Assuming mSectionIndexer.getPositionForSection(0) == 0.
824 sectionIndex = 0;
825 break;
826 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800827 }
828 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700829
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800830 // Find the next index, in case the assumed next index is not
Alan Viverettee918a482013-06-07 11:43:06 -0700831 // unique. For instance, if there is no P, then request for P's
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800832 // position actually returns Q's. So we need to look ahead to make
Alan Viverettee918a482013-06-07 11:43:06 -0700833 // sure that there is really a Q at Q's position. If not, move
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800834 // further down...
835 int nextNextSection = nextSection + 1;
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700836 while (nextNextSection < sectionCount &&
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800837 mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
838 nextNextSection++;
839 nextSection++;
840 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700841
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800842 // Compute the beginning and ending scroll range percentage of the
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700843 // currently visible section. This could be equal to or greater than
844 // (1 / nSections). If the target position is near the previous
845 // position, snap to the previous position.
846 final float prevPosition = (float) prevSection / sectionCount;
847 final float nextPosition = (float) nextSection / sectionCount;
848 final float snapThreshold = (count == 0) ? Float.MAX_VALUE : .125f / count;
849 if (prevSection == exactSection && position - prevPosition < snapThreshold) {
850 targetIndex = prevIndex;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800851 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700852 targetIndex = prevIndex + (int) ((nextIndex - prevIndex) * (position - prevPosition)
853 / (nextPosition - prevPosition));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800854 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700855
856 // Clamp to valid positions.
857 targetIndex = MathUtils.constrain(targetIndex, 0, count - 1);
Alan Viverettee918a482013-06-07 11:43:06 -0700858
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800859 if (mList instanceof ExpandableListView) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700860 final ExpandableListView expList = (ExpandableListView) mList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800861 expList.setSelectionFromTop(expList.getFlatListPosition(
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700862 ExpandableListView.getPackedPositionForGroup(targetIndex + mHeaderCount)),
863 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800864 } else if (mList instanceof ListView) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700865 ((ListView) mList).setSelectionFromTop(targetIndex + mHeaderCount, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800866 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700867 mList.setSelection(targetIndex + mHeaderCount);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800868 }
869 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700870 final int index = MathUtils.constrain((int) (position * count), 0, count - 1);
Adam Powell7ee1ff12011-03-09 16:35:13 -0800871
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800872 if (mList instanceof ExpandableListView) {
873 ExpandableListView expList = (ExpandableListView) mList;
874 expList.setSelectionFromTop(expList.getFlatListPosition(
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700875 ExpandableListView.getPackedPositionForGroup(index + mHeaderCount)), 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800876 } else if (mList instanceof ListView) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700877 ((ListView)mList).setSelectionFromTop(index + mHeaderCount, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800878 } else {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700879 mList.setSelection(index + mHeaderCount);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800880 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700881
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800882 sectionIndex = -1;
883 }
884
Alan Viverette6b40cc72013-06-25 16:41:52 -0700885 if (mCurrentSection != sectionIndex) {
886 mCurrentSection = sectionIndex;
887
888 if (transitionPreviewLayout(sectionIndex)) {
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700889 transitionToDragging();
Alan Viverette6b40cc72013-06-25 16:41:52 -0700890 } else {
891 transitionToVisible();
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700892 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800893 }
894 }
895
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700896 /**
Alan Viverette6b40cc72013-06-25 16:41:52 -0700897 * Transitions the preview text to a new section. Handles animation,
898 * measurement, and layout. If the new preview text is empty, returns false.
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700899 *
Alan Viverette6b40cc72013-06-25 16:41:52 -0700900 * @param sectionIndex The section index to which the preview should
901 * transition.
902 * @return False if the new preview text is empty.
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700903 */
Alan Viverette6b40cc72013-06-25 16:41:52 -0700904 private boolean transitionPreviewLayout(int sectionIndex) {
905 final Object[] sections = mSections;
906 String text = null;
907 if (sections != null && sectionIndex >= 0 && sectionIndex < sections.length) {
908 final Object section = sections[sectionIndex];
909 if (section != null) {
910 text = section.toString();
911 }
912 }
913
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700914 final Rect bounds = mTempBounds;
915 final ImageView preview = mPreviewImage;
916 final TextView showing;
917 final TextView target;
918 if (mShowingPrimary) {
919 showing = mPrimaryText;
920 target = mSecondaryText;
921 } else {
922 showing = mSecondaryText;
923 target = mPrimaryText;
924 }
925
926 // Set and layout target immediately.
927 target.setText(text);
928 measurePreview(target, bounds);
929 applyLayout(target, bounds);
930
931 if (mPreviewAnimation != null) {
932 mPreviewAnimation.cancel();
933 }
934
935 // Cross-fade preview text.
936 final Animator showTarget = animateAlpha(target, 1f).setDuration(DURATION_CROSS_FADE);
937 final Animator hideShowing = animateAlpha(showing, 0f).setDuration(DURATION_CROSS_FADE);
938 hideShowing.addListener(mSwitchPrimaryListener);
939
940 // Apply preview image padding and animate bounds, if necessary.
941 bounds.left -= mPreviewImage.getPaddingLeft();
942 bounds.top -= mPreviewImage.getPaddingTop();
943 bounds.right += mPreviewImage.getPaddingRight();
944 bounds.bottom += mPreviewImage.getPaddingBottom();
945 final Animator resizePreview = animateBounds(preview, bounds);
946 resizePreview.setDuration(DURATION_RESIZE);
947
948 mPreviewAnimation = new AnimatorSet();
949 final AnimatorSet.Builder builder = mPreviewAnimation.play(hideShowing).with(showTarget);
950 builder.with(resizePreview);
951
952 // The current preview size is unaffected by hidden or showing. It's
953 // used to set starting scales for things that need to be scaled down.
954 final int previewWidth = preview.getWidth() - preview.getPaddingLeft()
955 - preview.getPaddingRight();
956
957 // If target is too large, shrink it immediately to fit and expand to
958 // target size. Otherwise, start at target size.
959 final int targetWidth = target.getWidth();
960 if (targetWidth > previewWidth) {
961 target.setScaleX((float) previewWidth / targetWidth);
962 final Animator scaleAnim = animateScaleX(target, 1f).setDuration(DURATION_RESIZE);
963 builder.with(scaleAnim);
964 } else {
965 target.setScaleX(1f);
966 }
967
968 // If showing is larger than target, shrink to target size.
969 final int showingWidth = showing.getWidth();
970 if (showingWidth > targetWidth) {
971 final float scale = (float) targetWidth / showingWidth;
972 final Animator scaleAnim = animateScaleX(showing, scale).setDuration(DURATION_RESIZE);
973 builder.with(scaleAnim);
974 }
975
976 mPreviewAnimation.start();
Alan Viverette6b40cc72013-06-25 16:41:52 -0700977
978 return (text != null && text.length() > 0);
Alan Viverette0ebe81e2013-06-21 17:01:36 -0700979 }
980
981 /**
982 * Positions the thumb and preview widgets.
983 *
984 * @param position The position, between 0 and 1, along the track at which
985 * to place the thumb.
986 */
987 private void setThumbPos(float position) {
988 final int top = 0;
989 final int bottom = mList.getHeight();
990
991 final float thumbHalfHeight = mThumbImage.getHeight() / 2f;
992 final float min = top + thumbHalfHeight;
993 final float max = bottom - thumbHalfHeight;
994 final float offset = min;
995 final float range = max - min;
996 final float thumbMiddle = position * range + offset;
997 mThumbImage.setTranslationY(thumbMiddle - thumbHalfHeight);
998
999 // Center the preview on the thumb, constrained to the list bounds.
1000 final float previewHalfHeight = mPreviewImage.getHeight() / 2f;
1001 final float minP = top + previewHalfHeight;
1002 final float maxP = bottom - previewHalfHeight;
1003 final float previewMiddle = MathUtils.constrain(thumbMiddle, minP, maxP);
1004 final float previewTop = previewMiddle - previewHalfHeight;
1005
1006 mPreviewImage.setTranslationY(previewTop);
1007 mPrimaryText.setTranslationY(previewTop);
1008 mSecondaryText.setTranslationY(previewTop);
1009 }
1010
1011 private float getPosFromMotionEvent(float y) {
1012 final int top = 0;
1013 final int bottom = mList.getHeight();
1014
1015 final float thumbHalfHeight = mThumbImage.getHeight() / 2f;
1016 final float min = top + thumbHalfHeight;
1017 final float max = bottom - thumbHalfHeight;
1018 final float offset = min;
1019 final float range = max - min;
1020
1021 // If the list is the same height as the thumbnail or shorter,
1022 // effectively disable scrolling.
1023 if (range <= 0) {
1024 return 0f;
1025 }
1026
1027 return MathUtils.constrain((y - offset) / range, 0f, 1f);
1028 }
1029
1030 private float getPosFromItemCount(
1031 int firstVisibleItem, int visibleItemCount, int totalItemCount) {
Adam Powell35948b72011-08-25 14:15:59 -07001032 if (mSectionIndexer == null || mListAdapter == null) {
Adam Powell32c3a692011-01-09 21:28:43 -08001033 getSectionsFromIndexer();
1034 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001035
1036 final boolean hasSections = mSectionIndexer != null && mSections != null
1037 && mSections.length > 0;
1038 if (!hasSections || !mMatchDragPosition) {
Alan Viverette6b40cc72013-06-25 16:41:52 -07001039 return (float) firstVisibleItem / (totalItemCount - visibleItemCount);
Adam Powell32c3a692011-01-09 21:28:43 -08001040 }
1041
Alan Viverette6b40cc72013-06-25 16:41:52 -07001042 // Ignore headers.
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001043 firstVisibleItem -= mHeaderCount;
Adam Powell32c3a692011-01-09 21:28:43 -08001044 if (firstVisibleItem < 0) {
1045 return 0;
1046 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001047 totalItemCount -= mHeaderCount;
Adam Powell32c3a692011-01-09 21:28:43 -08001048
Alan Viverette6b40cc72013-06-25 16:41:52 -07001049 // Hidden portion of the first visible row.
1050 final View child = mList.getChildAt(0);
1051 final float incrementalPos;
1052 if (child == null || child.getHeight() == 0) {
1053 incrementalPos = 0;
1054 } else {
1055 incrementalPos = (float) (mList.getPaddingTop() - child.getTop()) / child.getHeight();
1056 }
1057
1058 // Number of rows in this section.
Adam Powell32c3a692011-01-09 21:28:43 -08001059 final int section = mSectionIndexer.getSectionForPosition(firstVisibleItem);
1060 final int sectionPos = mSectionIndexer.getPositionForSection(section);
Adam Powellf49971e2011-06-14 22:00:01 -07001061 final int sectionCount = mSections.length;
Alan Viverette6b40cc72013-06-25 16:41:52 -07001062 final int positionsInSection;
1063 if (section < sectionCount - 1) {
Jean-Baptiste Queru414b0232013-07-08 15:02:41 -07001064 final int nextSectionPos;
1065 if (section + 1 < sectionCount) {
1066 nextSectionPos = mSectionIndexer.getPositionForSection(section + 1);
1067 } else {
1068 nextSectionPos = totalItemCount - 1;
1069 }
Alan Viverette6b40cc72013-06-25 16:41:52 -07001070 positionsInSection = nextSectionPos - sectionPos;
1071 } else {
1072 positionsInSection = totalItemCount - sectionPos;
1073 }
Adam Powell32c3a692011-01-09 21:28:43 -08001074
Alan Viverette6b40cc72013-06-25 16:41:52 -07001075 // Position within this section.
1076 final float posWithinSection;
1077 if (positionsInSection == 0) {
1078 posWithinSection = 0;
1079 } else {
1080 posWithinSection = (firstVisibleItem + incrementalPos - sectionPos)
1081 / positionsInSection;
1082 }
1083
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001084 return (section + posWithinSection) / sectionCount;
Adam Powell32c3a692011-01-09 21:28:43 -08001085 }
1086
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001087 /**
1088 * Cancels an ongoing fling event by injecting a
1089 * {@link MotionEvent#ACTION_CANCEL} into the host view.
1090 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001091 private void cancelFling() {
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001092 final MotionEvent cancelFling = MotionEvent.obtain(
1093 0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001094 mList.onTouchEvent(cancelFling);
1095 cancelFling.recycle();
1096 }
Alan Viverettee918a482013-06-07 11:43:06 -07001097
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001098 /**
1099 * Cancels a pending drag.
1100 *
1101 * @see #startPendingDrag()
1102 */
1103 private void cancelPendingDrag() {
Adam Powellaf5280c2011-10-11 18:36:34 -07001104 mList.removeCallbacks(mDeferStartDrag);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001105 mHasPendingDrag = false;
Adam Powellaf5280c2011-10-11 18:36:34 -07001106 }
1107
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001108 /**
1109 * Delays dragging until after the framework has determined that the user is
1110 * scrolling, rather than tapping.
1111 */
1112 private void startPendingDrag() {
1113 mHasPendingDrag = true;
1114 mList.postDelayed(mDeferStartDrag, TAP_TIMEOUT);
Adam Powellaf5280c2011-10-11 18:36:34 -07001115 }
1116
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001117 private void beginDrag() {
Adam Powellaf5280c2011-10-11 18:36:34 -07001118 setState(STATE_DRAGGING);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001119
Adam Powellaf5280c2011-10-11 18:36:34 -07001120 if (mListAdapter == null && mList != null) {
1121 getSectionsFromIndexer();
1122 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001123
Adam Powellaf5280c2011-10-11 18:36:34 -07001124 if (mList != null) {
1125 mList.requestDisallowInterceptTouchEvent(true);
1126 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
1127 }
1128
1129 cancelFling();
1130 }
1131
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001132 public boolean onInterceptTouchEvent(MotionEvent ev) {
Alan Viverette447cdf22013-07-15 17:47:34 -07001133 if (!mEnabled) {
1134 return false;
1135 }
1136
Adam Powellaf5280c2011-10-11 18:36:34 -07001137 switch (ev.getActionMasked()) {
1138 case MotionEvent.ACTION_DOWN:
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001139 if (isPointInside(ev.getX(), ev.getY())) {
1140 // If the parent has requested that its children delay
1141 // pressed state (e.g. is a scrolling container) then we
1142 // need to allow the parent time to decide whether it wants
1143 // to intercept events. If it does, we will receive a CANCEL
1144 // event.
1145 if (mList.isInScrollingContainer()) {
1146 mInitialTouchY = ev.getY();
1147 startPendingDrag();
1148 return false;
Adam Powellaf5280c2011-10-11 18:36:34 -07001149 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001150
1151 beginDrag();
1152 return true;
Adam Powellaf5280c2011-10-11 18:36:34 -07001153 }
1154 break;
1155 case MotionEvent.ACTION_UP:
1156 case MotionEvent.ACTION_CANCEL:
1157 cancelPendingDrag();
1158 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001159 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001160
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001161 return false;
1162 }
1163
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001164 public boolean onTouchEvent(MotionEvent me) {
Alan Viverette447cdf22013-07-15 17:47:34 -07001165 if (!mEnabled) {
1166 return false;
1167 }
1168
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001169 switch (me.getActionMasked()) {
1170 case MotionEvent.ACTION_DOWN: {
1171 if (isPointInside(me.getX(), me.getY())) {
Adam Powellaf5280c2011-10-11 18:36:34 -07001172 beginDrag();
1173 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001174 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001175 } break;
Adam Powellaf5280c2011-10-11 18:36:34 -07001176
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001177 case MotionEvent.ACTION_UP: {
1178 if (mHasPendingDrag) {
1179 // Allow a tap to scroll.
1180 beginDrag();
Adam Powellaf5280c2011-10-11 18:36:34 -07001181
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001182 final float pos = getPosFromMotionEvent(me.getY());
1183 setThumbPos(pos);
1184 scrollTo(pos);
1185
1186 cancelPendingDrag();
1187 // Will hit the STATE_DRAGGING check below
Adam Powell20232d02010-12-08 21:08:53 -08001188 }
1189
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001190 if (mState == STATE_DRAGGING) {
1191 if (mList != null) {
1192 // ViewGroup does the right thing already, but there might
1193 // be other classes that don't properly reset on touch-up,
1194 // so do this explicitly just in case.
1195 mList.requestDisallowInterceptTouchEvent(false);
1196 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1197 }
1198
1199 setState(STATE_VISIBLE);
1200 mList.postDelayed(mDeferHide, FADE_TIMEOUT);
1201
1202 return true;
1203 }
1204 } break;
1205
1206 case MotionEvent.ACTION_MOVE: {
1207 if (mHasPendingDrag && Math.abs(me.getY() - mInitialTouchY) > mScaledTouchSlop) {
Adam Powellaf5280c2011-10-11 18:36:34 -07001208 setState(STATE_DRAGGING);
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001209
Adam Powellaf5280c2011-10-11 18:36:34 -07001210 if (mListAdapter == null && mList != null) {
1211 getSectionsFromIndexer();
1212 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001213
Adam Powellaf5280c2011-10-11 18:36:34 -07001214 if (mList != null) {
1215 mList.requestDisallowInterceptTouchEvent(true);
1216 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
1217 }
1218
1219 cancelFling();
1220 cancelPendingDrag();
1221 // Will hit the STATE_DRAGGING check below
1222 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001223
1224 if (mState == STATE_DRAGGING) {
1225 // TODO: Ignore jitter.
1226 final float pos = getPosFromMotionEvent(me.getY());
1227 setThumbPos(pos);
1228
1229 // If the previous scrollTo is still pending
1230 if (mScrollCompleted) {
1231 scrollTo(pos);
1232 }
1233
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001234 return true;
1235 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001236 } break;
1237
1238 case MotionEvent.ACTION_CANCEL: {
1239 cancelPendingDrag();
1240 } break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001241 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001242
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001243 return false;
1244 }
Romain Guyd6a463a2009-05-21 23:10:10 -07001245
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001246 /**
1247 * Returns whether a coordinate is inside the scroller's activation area. If
1248 * there is a track image, touching anywhere within the thumb-width of the
1249 * track activates scrolling. Otherwise, the user has to touch inside thumb
1250 * itself.
1251 *
1252 * @param x The x-coordinate.
1253 * @param y The y-coordinate.
1254 * @return Whether the coordinate is inside the scroller's activation area.
1255 */
1256 private boolean isPointInside(float x, float y) {
1257 return isPointInsideX(x) && (mHasTrackImage || isPointInsideY(y));
Romain Guy82f34952009-05-24 18:40:45 -07001258 }
1259
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001260 private boolean isPointInsideX(float x) {
1261 if (mLayoutFromRight) {
1262 return x >= mThumbImage.getLeft();
1263 } else {
1264 return x <= mThumbImage.getRight();
1265 }
1266 }
Alan Viverettee918a482013-06-07 11:43:06 -07001267
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001268 private boolean isPointInsideY(float y) {
1269 return y >= mThumbImage.getTop() && y <= mThumbImage.getBottom();
1270 }
Alan Viverettee918a482013-06-07 11:43:06 -07001271
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001272 /**
1273 * Constructs an animator for the specified property on a group of views.
1274 * See {@link ObjectAnimator#ofFloat(Object, String, float...)} for
1275 * implementation details.
1276 *
1277 * @param property The property being animated.
1278 * @param value The value to which that property should animate.
1279 * @param views The target views to animate.
1280 * @return An animator for all the specified views.
1281 */
1282 private static Animator groupAnimatorOfFloat(
1283 Property<View, Float> property, float value, View... views) {
1284 AnimatorSet animSet = new AnimatorSet();
1285 AnimatorSet.Builder builder = null;
1286
1287 for (int i = views.length - 1; i >= 0; i--) {
1288 final Animator anim = ObjectAnimator.ofFloat(views[i], property, value);
1289 if (builder == null) {
1290 builder = animSet.play(anim);
1291 } else {
1292 builder.with(anim);
1293 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001294 }
Alan Viverettee918a482013-06-07 11:43:06 -07001295
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001296 return animSet;
1297 }
1298
1299 /**
1300 * Returns an animator for the view's scaleX value.
1301 */
1302 private static Animator animateScaleX(View v, float target) {
1303 return ObjectAnimator.ofFloat(v, View.SCALE_X, target);
1304 }
1305
1306 /**
1307 * Returns an animator for the view's alpha value.
1308 */
1309 private static Animator animateAlpha(View v, float alpha) {
1310 return ObjectAnimator.ofFloat(v, View.ALPHA, alpha);
1311 }
1312
1313 /**
1314 * A Property wrapper around the <code>left</code> functionality handled by the
1315 * {@link View#setLeft(int)} and {@link View#getLeft()} methods.
1316 */
1317 private static Property<View, Integer> LEFT = new IntProperty<View>("left") {
1318 @Override
1319 public void setValue(View object, int value) {
1320 object.setLeft(value);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001321 }
Alan Viverettee918a482013-06-07 11:43:06 -07001322
1323 @Override
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001324 public Integer get(View object) {
1325 return object.getLeft();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001326 }
Alan Viverette0ebe81e2013-06-21 17:01:36 -07001327 };
1328
1329 /**
1330 * A Property wrapper around the <code>top</code> functionality handled by the
1331 * {@link View#setTop(int)} and {@link View#getTop()} methods.
1332 */
1333 private static Property<View, Integer> TOP = new IntProperty<View>("top") {
1334 @Override
1335 public void setValue(View object, int value) {
1336 object.setTop(value);
1337 }
1338
1339 @Override
1340 public Integer get(View object) {
1341 return object.getTop();
1342 }
1343 };
1344
1345 /**
1346 * A Property wrapper around the <code>right</code> functionality handled by the
1347 * {@link View#setRight(int)} and {@link View#getRight()} methods.
1348 */
1349 private static Property<View, Integer> RIGHT = new IntProperty<View>("right") {
1350 @Override
1351 public void setValue(View object, int value) {
1352 object.setRight(value);
1353 }
1354
1355 @Override
1356 public Integer get(View object) {
1357 return object.getRight();
1358 }
1359 };
1360
1361 /**
1362 * A Property wrapper around the <code>bottom</code> functionality handled by the
1363 * {@link View#setBottom(int)} and {@link View#getBottom()} methods.
1364 */
1365 private static Property<View, Integer> BOTTOM = new IntProperty<View>("bottom") {
1366 @Override
1367 public void setValue(View object, int value) {
1368 object.setBottom(value);
1369 }
1370
1371 @Override
1372 public Integer get(View object) {
1373 return object.getBottom();
1374 }
1375 };
1376
1377 /**
1378 * Returns an animator for the view's bounds.
1379 */
1380 private static Animator animateBounds(View v, Rect bounds) {
1381 final PropertyValuesHolder left = PropertyValuesHolder.ofInt(LEFT, bounds.left);
1382 final PropertyValuesHolder top = PropertyValuesHolder.ofInt(TOP, bounds.top);
1383 final PropertyValuesHolder right = PropertyValuesHolder.ofInt(RIGHT, bounds.right);
1384 final PropertyValuesHolder bottom = PropertyValuesHolder.ofInt(BOTTOM, bounds.bottom);
1385 return ObjectAnimator.ofPropertyValuesHolder(v, left, top, right, bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001386 }
1387}