blob: 10b8cbe3848cdc2c9abc16f13235625faccd9374 [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
19import android.content.Context;
20import android.content.res.ColorStateList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.content.res.TypedArray;
22import android.graphics.Canvas;
23import android.graphics.Paint;
Adam Powell20232d02010-12-08 21:08:53 -080024import android.graphics.Rect;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025import android.graphics.RectF;
26import android.graphics.drawable.Drawable;
Adam Powell20232d02010-12-08 21:08:53 -080027import android.graphics.drawable.NinePatchDrawable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028import android.os.Handler;
29import android.os.SystemClock;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030import android.view.MotionEvent;
Adam Powell20232d02010-12-08 21:08:53 -080031import android.view.View;
Adam Powellaf5280c2011-10-11 18:36:34 -070032import android.view.ViewConfiguration;
Adam Powelld43bd482010-02-26 16:29:09 -080033import android.widget.AbsListView.OnScrollListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080034
35/**
36 * Helper class for AbsListView to draw and control the Fast Scroll thumb
37 */
38class FastScroller {
Adam Powellaf5280c2011-10-11 18:36:34 -070039 private static final String TAG = "FastScroller";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040
41 // Minimum number of pages to justify showing a fast scroll thumb
42 private static int MIN_PAGES = 4;
43 // Scroll thumb not showing
44 private static final int STATE_NONE = 0;
45 // Not implemented yet - fade-in transition
46 private static final int STATE_ENTER = 1;
47 // Scroll thumb visible and moving along with the scrollbar
48 private static final int STATE_VISIBLE = 2;
49 // Scroll thumb being dragged by user
50 private static final int STATE_DRAGGING = 3;
51 // Scroll thumb fading out due to inactivity timeout
52 private static final int STATE_EXIT = 4;
Adam Powell20232d02010-12-08 21:08:53 -080053
54 private static final int[] PRESSED_STATES = new int[] {
55 android.R.attr.state_pressed
56 };
57
58 private static final int[] DEFAULT_STATES = new int[0];
59
60 private static final int[] ATTRS = new int[] {
Adam Powellb2e55172011-01-15 17:21:35 -080061 android.R.attr.fastScrollTextColor,
Adam Powell128b6ba2010-12-13 12:33:44 -080062 android.R.attr.fastScrollThumbDrawable,
63 android.R.attr.fastScrollTrackDrawable,
64 android.R.attr.fastScrollPreviewBackgroundLeft,
65 android.R.attr.fastScrollPreviewBackgroundRight,
66 android.R.attr.fastScrollOverlayPosition
Adam Powell20232d02010-12-08 21:08:53 -080067 };
68
Adam Powellb2e55172011-01-15 17:21:35 -080069 private static final int TEXT_COLOR = 0;
Adam Powell20232d02010-12-08 21:08:53 -080070 private static final int THUMB_DRAWABLE = 1;
71 private static final int TRACK_DRAWABLE = 2;
72 private static final int PREVIEW_BACKGROUND_LEFT = 3;
73 private static final int PREVIEW_BACKGROUND_RIGHT = 4;
74 private static final int OVERLAY_POSITION = 5;
75
76 private static final int OVERLAY_FLOATING = 0;
77 private static final int OVERLAY_AT_THUMB = 1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080078
79 private Drawable mThumbDrawable;
80 private Drawable mOverlayDrawable;
Adam Powell20232d02010-12-08 21:08:53 -080081 private Drawable mTrackDrawable;
82
83 private Drawable mOverlayDrawableLeft;
84 private Drawable mOverlayDrawableRight;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080085
Adam Powellaf5280c2011-10-11 18:36:34 -070086 int mThumbH;
87 int mThumbW;
88 int mThumbY;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080089
90 private RectF mOverlayPos;
Amith Yamasani1ffaebc2009-08-20 16:15:46 -070091 private int mOverlaySize;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080092
Adam Powellaf5280c2011-10-11 18:36:34 -070093 AbsListView mList;
94 boolean mScrollCompleted;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080095 private int mVisibleItem;
96 private Paint mPaint;
97 private int mListOffset;
The Android Open Source Project4df24232009-03-05 14:34:35 -080098 private int mItemCount = -1;
99 private boolean mLongList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800100
101 private Object [] mSections;
102 private String mSectionText;
103 private boolean mDrawOverlay;
104 private ScrollFade mScrollFade;
105
106 private int mState;
107
108 private Handler mHandler = new Handler();
109
Adam Powellaf5280c2011-10-11 18:36:34 -0700110 BaseAdapter mListAdapter;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800111 private SectionIndexer mSectionIndexer;
112
113 private boolean mChangedBounds;
114
Adam Powell20232d02010-12-08 21:08:53 -0800115 private int mPosition;
116
117 private boolean mAlwaysShow;
118
119 private int mOverlayPosition;
120
Adam Powell568ccd82011-08-03 22:38:48 -0700121 private boolean mMatchDragPosition;
122
Adam Powellaf5280c2011-10-11 18:36:34 -0700123 float mInitialTouchY;
124 boolean mPendingDrag;
125 private int mScaledTouchSlop;
126
Adam Powell20232d02010-12-08 21:08:53 -0800127 private static final int FADE_TIMEOUT = 1500;
Adam Powellaf5280c2011-10-11 18:36:34 -0700128 private static final int PENDING_DRAG_DELAY = 180;
Adam Powell20232d02010-12-08 21:08:53 -0800129
130 private final Rect mTmpRect = new Rect();
131
Adam Powellaf5280c2011-10-11 18:36:34 -0700132 private final Runnable mDeferStartDrag = new Runnable() {
133 public void run() {
134 if (mList.mIsAttached) {
135 beginDrag();
136
137 final int viewHeight = mList.getHeight();
138 // Jitter
139 int newThumbY = (int) mInitialTouchY - mThumbH + 10;
140 if (newThumbY < 0) {
141 newThumbY = 0;
142 } else if (newThumbY + mThumbH > viewHeight) {
143 newThumbY = viewHeight - mThumbH;
144 }
145 mThumbY = newThumbY;
146 scrollTo((float) mThumbY / (viewHeight - mThumbH));
147 }
148
149 mPendingDrag = false;
150 }
151 };
152
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800153 public FastScroller(Context context, AbsListView listView) {
154 mList = listView;
155 init(context);
156 }
157
Adam Powell20232d02010-12-08 21:08:53 -0800158 public void setAlwaysShow(boolean alwaysShow) {
159 mAlwaysShow = alwaysShow;
160 if (alwaysShow) {
161 mHandler.removeCallbacks(mScrollFade);
162 setState(STATE_VISIBLE);
163 } else if (mState == STATE_VISIBLE) {
164 mHandler.postDelayed(mScrollFade, FADE_TIMEOUT);
165 }
166 }
167
168 public boolean isAlwaysShowEnabled() {
169 return mAlwaysShow;
170 }
171
172 private void refreshDrawableState() {
173 int[] state = mState == STATE_DRAGGING ? PRESSED_STATES : DEFAULT_STATES;
174
175 if (mThumbDrawable != null && mThumbDrawable.isStateful()) {
176 mThumbDrawable.setState(state);
177 }
178 if (mTrackDrawable != null && mTrackDrawable.isStateful()) {
179 mTrackDrawable.setState(state);
180 }
181 }
182
183 public void setScrollbarPosition(int position) {
Fabrice Di Meglioc23ee462012-06-22 18:46:06 -0700184 if (position == View.SCROLLBAR_POSITION_DEFAULT) {
185 position = mList.isLayoutRtl() ?
186 View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT;
187 }
Adam Powell20232d02010-12-08 21:08:53 -0800188 mPosition = position;
189 switch (position) {
190 default:
Adam Powell20232d02010-12-08 21:08:53 -0800191 case View.SCROLLBAR_POSITION_RIGHT:
192 mOverlayDrawable = mOverlayDrawableRight;
193 break;
194 case View.SCROLLBAR_POSITION_LEFT:
195 mOverlayDrawable = mOverlayDrawableLeft;
196 break;
197 }
198 }
199
200 public int getWidth() {
201 return mThumbW;
202 }
203
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800204 public void setState(int state) {
205 switch (state) {
206 case STATE_NONE:
207 mHandler.removeCallbacks(mScrollFade);
208 mList.invalidate();
209 break;
210 case STATE_VISIBLE:
211 if (mState != STATE_VISIBLE) { // Optimization
212 resetThumbPos();
213 }
214 // Fall through
215 case STATE_DRAGGING:
216 mHandler.removeCallbacks(mScrollFade);
217 break;
218 case STATE_EXIT:
Fabrice Di Meglio3a1f1e52013-04-16 15:40:18 -0700219 final int viewWidth = mList.getWidth();
220 final int top = mThumbY;
221 final int bottom = mThumbY + mThumbH;
222 final int left;
223 final int right;
224 switch (mList.getLayoutDirection()) {
225 case View.LAYOUT_DIRECTION_RTL:
226 left = 0;
227 right = mThumbW;
228 break;
229 case View.LAYOUT_DIRECTION_LTR:
230 default:
231 left = viewWidth - mThumbW;
232 right = viewWidth;
233 }
234 mList.invalidate(left, top, right, bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800235 break;
236 }
237 mState = state;
Adam Powell20232d02010-12-08 21:08:53 -0800238 refreshDrawableState();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800239 }
240
241 public int getState() {
242 return mState;
243 }
244
245 private void resetThumbPos() {
246 final int viewWidth = mList.getWidth();
247 // Bounds are always top right. Y coordinate get's translated during draw
Adam Powell20232d02010-12-08 21:08:53 -0800248 switch (mPosition) {
Adam Powell20232d02010-12-08 21:08:53 -0800249 case View.SCROLLBAR_POSITION_RIGHT:
250 mThumbDrawable.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH);
251 break;
252 case View.SCROLLBAR_POSITION_LEFT:
253 mThumbDrawable.setBounds(0, 0, mThumbW, mThumbH);
254 break;
255 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800256 mThumbDrawable.setAlpha(ScrollFade.ALPHA_MAX);
257 }
258
Mitsuru Oshimaa5e44152009-07-14 10:37:22 -0700259 private void useThumbDrawable(Context context, Drawable drawable) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800260 mThumbDrawable = drawable;
Adam Powell20232d02010-12-08 21:08:53 -0800261 if (drawable instanceof NinePatchDrawable) {
262 mThumbW = context.getResources().getDimensionPixelSize(
263 com.android.internal.R.dimen.fastscroll_thumb_width);
264 mThumbH = context.getResources().getDimensionPixelSize(
265 com.android.internal.R.dimen.fastscroll_thumb_height);
266 } else {
267 mThumbW = drawable.getIntrinsicWidth();
268 mThumbH = drawable.getIntrinsicHeight();
269 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800270 mChangedBounds = true;
271 }
272
273 private void init(Context context) {
274 // Get both the scrollbar states drawables
Adam Powell20232d02010-12-08 21:08:53 -0800275 TypedArray ta = context.getTheme().obtainStyledAttributes(ATTRS);
Adam Powell128b6ba2010-12-13 12:33:44 -0800276 useThumbDrawable(context, ta.getDrawable(THUMB_DRAWABLE));
277 mTrackDrawable = ta.getDrawable(TRACK_DRAWABLE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800278
Adam Powell128b6ba2010-12-13 12:33:44 -0800279 mOverlayDrawableLeft = ta.getDrawable(PREVIEW_BACKGROUND_LEFT);
280 mOverlayDrawableRight = ta.getDrawable(PREVIEW_BACKGROUND_RIGHT);
281 mOverlayPosition = ta.getInt(OVERLAY_POSITION, OVERLAY_FLOATING);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800282
283 mScrollCompleted = true;
284
Romain Guyd6a463a2009-05-21 23:10:10 -0700285 getSectionsFromIndexer();
Amith Yamasani1ffaebc2009-08-20 16:15:46 -0700286
287 mOverlaySize = context.getResources().getDimensionPixelSize(
288 com.android.internal.R.dimen.fastscroll_overlay_size);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800289 mOverlayPos = new RectF();
290 mScrollFade = new ScrollFade();
291 mPaint = new Paint();
292 mPaint.setAntiAlias(true);
293 mPaint.setTextAlign(Paint.Align.CENTER);
294 mPaint.setTextSize(mOverlaySize / 2);
Adam Powell20232d02010-12-08 21:08:53 -0800295
Adam Powellb2e55172011-01-15 17:21:35 -0800296 ColorStateList textColor = ta.getColorStateList(TEXT_COLOR);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800297 int textColorNormal = textColor.getDefaultColor();
298 mPaint.setColor(textColorNormal);
299 mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
NoraBora9b38c602010-10-12 06:59:55 -0700300
301 // to show mOverlayDrawable properly
302 if (mList.getWidth() > 0 && mList.getHeight() > 0) {
303 onSizeChanged(mList.getWidth(), mList.getHeight(), 0, 0);
304 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800305
306 mState = STATE_NONE;
Adam Powell20232d02010-12-08 21:08:53 -0800307 refreshDrawableState();
308
309 ta.recycle();
310
Adam Powellaf5280c2011-10-11 18:36:34 -0700311 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
312
Adam Powell568ccd82011-08-03 22:38:48 -0700313 mMatchDragPosition = context.getApplicationInfo().targetSdkVersion >=
314 android.os.Build.VERSION_CODES.HONEYCOMB;
315
Adam Powell20232d02010-12-08 21:08:53 -0800316 setScrollbarPosition(mList.getVerticalScrollbarPosition());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800317 }
318
319 void stop() {
320 setState(STATE_NONE);
321 }
322
323 boolean isVisible() {
324 return !(mState == STATE_NONE);
325 }
326
327 public void draw(Canvas canvas) {
328
329 if (mState == STATE_NONE) {
330 // No need to draw anything
331 return;
332 }
333
334 final int y = mThumbY;
335 final int viewWidth = mList.getWidth();
336 final FastScroller.ScrollFade scrollFade = mScrollFade;
337
338 int alpha = -1;
339 if (mState == STATE_EXIT) {
340 alpha = scrollFade.getAlpha();
341 if (alpha < ScrollFade.ALPHA_MAX / 2) {
342 mThumbDrawable.setAlpha(alpha * 2);
343 }
Adam Powell20232d02010-12-08 21:08:53 -0800344 int left = 0;
345 switch (mPosition) {
Adam Powell20232d02010-12-08 21:08:53 -0800346 case View.SCROLLBAR_POSITION_RIGHT:
347 left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
348 break;
349 case View.SCROLLBAR_POSITION_LEFT:
350 left = -mThumbW + (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
351 break;
352 }
353 mThumbDrawable.setBounds(left, 0, left + mThumbW, mThumbH);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800354 mChangedBounds = true;
355 }
356
Adam Powell20232d02010-12-08 21:08:53 -0800357 if (mTrackDrawable != null) {
Adam Powell2c6196a2010-12-10 14:31:54 -0800358 final Rect thumbBounds = mThumbDrawable.getBounds();
359 final int left = thumbBounds.left;
360 final int halfThumbHeight = (thumbBounds.bottom - thumbBounds.top) / 2;
Adam Powell20232d02010-12-08 21:08:53 -0800361 final int trackWidth = mTrackDrawable.getIntrinsicWidth();
Adam Powell2c6196a2010-12-10 14:31:54 -0800362 final int trackLeft = (left + mThumbW / 2) - trackWidth / 2;
363 mTrackDrawable.setBounds(trackLeft, halfThumbHeight,
364 trackLeft + trackWidth, mList.getHeight() - halfThumbHeight);
Adam Powell20232d02010-12-08 21:08:53 -0800365 mTrackDrawable.draw(canvas);
366 }
367
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800368 canvas.translate(0, y);
369 mThumbDrawable.draw(canvas);
370 canvas.translate(0, -y);
371
372 // If user is dragging the scroll bar, draw the alphabet overlay
373 if (mState == STATE_DRAGGING && mDrawOverlay) {
Adam Powell20232d02010-12-08 21:08:53 -0800374 if (mOverlayPosition == OVERLAY_AT_THUMB) {
375 int left = 0;
376 switch (mPosition) {
377 default:
Adam Powell20232d02010-12-08 21:08:53 -0800378 case View.SCROLLBAR_POSITION_RIGHT:
379 left = Math.max(0,
380 mThumbDrawable.getBounds().left - mThumbW - mOverlaySize);
381 break;
382 case View.SCROLLBAR_POSITION_LEFT:
383 left = Math.min(mThumbDrawable.getBounds().right + mThumbW,
384 mList.getWidth() - mOverlaySize);
385 break;
386 }
387
388 int top = Math.max(0,
389 Math.min(y + (mThumbH - mOverlaySize) / 2, mList.getHeight() - mOverlaySize));
390
391 final RectF pos = mOverlayPos;
392 pos.left = left;
393 pos.right = pos.left + mOverlaySize;
394 pos.top = top;
395 pos.bottom = pos.top + mOverlaySize;
396 if (mOverlayDrawable != null) {
397 mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
398 (int) pos.right, (int) pos.bottom);
399 }
400 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800401 mOverlayDrawable.draw(canvas);
402 final Paint paint = mPaint;
403 float descent = paint.descent();
404 final RectF rectF = mOverlayPos;
Adam Powell20232d02010-12-08 21:08:53 -0800405 final Rect tmpRect = mTmpRect;
406 mOverlayDrawable.getPadding(tmpRect);
407 final int hOff = (tmpRect.right - tmpRect.left) / 2;
408 final int vOff = (tmpRect.bottom - tmpRect.top) / 2;
409 canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2 - hOff,
410 (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent - vOff,
411 paint);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800412 } else if (mState == STATE_EXIT) {
413 if (alpha == 0) { // Done with exit
414 setState(STATE_NONE);
415 } else {
Fabrice Di Meglio3a1f1e52013-04-16 15:40:18 -0700416 final int left, right, top, bottom;
417 if (mTrackDrawable != null) {
418 top = 0;
419 bottom = mList.getHeight();
420 } else {
421 top = y;
422 bottom = y + mThumbH;
423 }
424 switch (mList.getLayoutDirection()) {
425 case View.LAYOUT_DIRECTION_RTL:
426 left = 0;
427 right = mThumbW;
428 break;
429 case View.LAYOUT_DIRECTION_LTR:
430 default:
431 left = viewWidth - mThumbW;
432 right = viewWidth;
433 }
434 mList.invalidate(left, top, right, bottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800435 }
436 }
437 }
438
439 void onSizeChanged(int w, int h, int oldw, int oldh) {
440 if (mThumbDrawable != null) {
Adam Powell20232d02010-12-08 21:08:53 -0800441 switch (mPosition) {
442 default:
Adam Powell20232d02010-12-08 21:08:53 -0800443 case View.SCROLLBAR_POSITION_RIGHT:
444 mThumbDrawable.setBounds(w - mThumbW, 0, w, mThumbH);
445 break;
446 case View.SCROLLBAR_POSITION_LEFT:
447 mThumbDrawable.setBounds(0, 0, mThumbW, mThumbH);
448 break;
449 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800450 }
Adam Powell20232d02010-12-08 21:08:53 -0800451 if (mOverlayPosition == OVERLAY_FLOATING) {
452 final RectF pos = mOverlayPos;
453 pos.left = (w - mOverlaySize) / 2;
454 pos.right = pos.left + mOverlaySize;
455 pos.top = h / 10; // 10% from top
456 pos.bottom = pos.top + mOverlaySize;
457 if (mOverlayDrawable != null) {
458 mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
459 (int) pos.right, (int) pos.bottom);
460 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800461 }
462 }
Adam Powell2c6196a2010-12-10 14:31:54 -0800463
464 void onItemCountChanged(int oldCount, int newCount) {
465 if (mAlwaysShow) {
466 mLongList = true;
467 }
468 }
469
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800470 void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
471 int totalItemCount) {
The Android Open Source Project4df24232009-03-05 14:34:35 -0800472 // Are there enough pages to require fast scroll? Recompute only if total count changes
473 if (mItemCount != totalItemCount && visibleItemCount > 0) {
474 mItemCount = totalItemCount;
Adam Powell32c3a692011-01-09 21:28:43 -0800475 mLongList = mItemCount / visibleItemCount >= MIN_PAGES;
476 }
477 if (mAlwaysShow) {
478 mLongList = true;
The Android Open Source Project4df24232009-03-05 14:34:35 -0800479 }
480 if (!mLongList) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800481 if (mState != STATE_NONE) {
482 setState(STATE_NONE);
483 }
484 return;
485 }
Adam Powell568ccd82011-08-03 22:38:48 -0700486 if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING) {
Adam Powell32c3a692011-01-09 21:28:43 -0800487 mThumbY = getThumbPositionForListPosition(firstVisibleItem, visibleItemCount,
488 totalItemCount);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800489 if (mChangedBounds) {
490 resetThumbPos();
491 mChangedBounds = false;
492 }
493 }
494 mScrollCompleted = true;
495 if (firstVisibleItem == mVisibleItem) {
496 return;
497 }
498 mVisibleItem = firstVisibleItem;
499 if (mState != STATE_DRAGGING) {
500 setState(STATE_VISIBLE);
Adam Powell20232d02010-12-08 21:08:53 -0800501 if (!mAlwaysShow) {
502 mHandler.postDelayed(mScrollFade, FADE_TIMEOUT);
503 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800504 }
505 }
506
Romain Guyd6a463a2009-05-21 23:10:10 -0700507 SectionIndexer getSectionIndexer() {
508 return mSectionIndexer;
509 }
510
511 Object[] getSections() {
512 if (mListAdapter == null && mList != null) {
513 getSectionsFromIndexer();
514 }
515 return mSections;
516 }
517
Adam Powellaf5280c2011-10-11 18:36:34 -0700518 void getSectionsFromIndexer() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800519 Adapter adapter = mList.getAdapter();
520 mSectionIndexer = null;
521 if (adapter instanceof HeaderViewListAdapter) {
522 mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
523 adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
524 }
525 if (adapter instanceof ExpandableListConnector) {
526 ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter();
527 if (expAdapter instanceof SectionIndexer) {
528 mSectionIndexer = (SectionIndexer) expAdapter;
529 mListAdapter = (BaseAdapter) adapter;
530 mSections = mSectionIndexer.getSections();
531 }
532 } else {
533 if (adapter instanceof SectionIndexer) {
534 mListAdapter = (BaseAdapter) adapter;
535 mSectionIndexer = (SectionIndexer) adapter;
536 mSections = mSectionIndexer.getSections();
Adam Powellf49971e2011-06-14 22:00:01 -0700537 if (mSections == null) {
538 mSections = new String[] { " " };
539 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800540 } else {
541 mListAdapter = (BaseAdapter) adapter;
542 mSections = new String[] { " " };
543 }
544 }
545 }
546
Adam Powellb1f498a2011-01-18 20:43:23 -0800547 public void onSectionsChanged() {
548 mListAdapter = null;
549 }
550
Adam Powellaf5280c2011-10-11 18:36:34 -0700551 void scrollTo(float position) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800552 int count = mList.getCount();
553 mScrollCompleted = false;
554 float fThreshold = (1.0f / count) / 8;
555 final Object[] sections = mSections;
556 int sectionIndex;
557 if (sections != null && sections.length > 1) {
558 final int nSections = sections.length;
559 int section = (int) (position * nSections);
560 if (section >= nSections) {
561 section = nSections - 1;
562 }
563 int exactSection = section;
564 sectionIndex = section;
565 int index = mSectionIndexer.getPositionForSection(section);
566 // Given the expected section and index, the following code will
567 // try to account for missing sections (no names starting with..)
568 // It will compute the scroll space of surrounding empty sections
569 // and interpolate the currently visible letter's range across the
570 // available space, so that there is always some list movement while
571 // the user moves the thumb.
572 int nextIndex = count;
573 int prevIndex = index;
574 int prevSection = section;
575 int nextSection = section + 1;
576 // Assume the next section is unique
577 if (section < nSections - 1) {
578 nextIndex = mSectionIndexer.getPositionForSection(section + 1);
579 }
580
581 // Find the previous index if we're slicing the previous section
582 if (nextIndex == index) {
583 // Non-existent letter
584 while (section > 0) {
585 section--;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700586 prevIndex = mSectionIndexer.getPositionForSection(section);
587 if (prevIndex != index) {
588 prevSection = section;
589 sectionIndex = section;
590 break;
591 } else if (section == 0) {
592 // When section reaches 0 here, sectionIndex must follow it.
593 // Assuming mSectionIndexer.getPositionForSection(0) == 0.
594 sectionIndex = 0;
595 break;
596 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800597 }
598 }
599 // Find the next index, in case the assumed next index is not
600 // unique. For instance, if there is no P, then request for P's
601 // position actually returns Q's. So we need to look ahead to make
602 // sure that there is really a Q at Q's position. If not, move
603 // further down...
604 int nextNextSection = nextSection + 1;
605 while (nextNextSection < nSections &&
606 mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
607 nextNextSection++;
608 nextSection++;
609 }
610 // Compute the beginning and ending scroll range percentage of the
611 // currently visible letter. This could be equal to or greater than
612 // (1 / nSections).
613 float fPrev = (float) prevSection / nSections;
614 float fNext = (float) nextSection / nSections;
615 if (prevSection == exactSection && position - fPrev < fThreshold) {
616 index = prevIndex;
617 } else {
618 index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev)
619 / (fNext - fPrev));
620 }
621 // Don't overflow
622 if (index > count - 1) index = count - 1;
623
624 if (mList instanceof ExpandableListView) {
625 ExpandableListView expList = (ExpandableListView) mList;
626 expList.setSelectionFromTop(expList.getFlatListPosition(
627 ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
628 } else if (mList instanceof ListView) {
629 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
630 } else {
631 mList.setSelection(index + mListOffset);
632 }
633 } else {
634 int index = (int) (position * count);
Adam Powell7ee1ff12011-03-09 16:35:13 -0800635 // Don't overflow
636 if (index > count - 1) index = count - 1;
637
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800638 if (mList instanceof ExpandableListView) {
639 ExpandableListView expList = (ExpandableListView) mList;
640 expList.setSelectionFromTop(expList.getFlatListPosition(
641 ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
642 } else if (mList instanceof ListView) {
643 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
644 } else {
645 mList.setSelection(index + mListOffset);
646 }
647 sectionIndex = -1;
648 }
649
650 if (sectionIndex >= 0) {
651 String text = mSectionText = sections[sectionIndex].toString();
652 mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') &&
653 sectionIndex < sections.length;
654 } else {
655 mDrawOverlay = false;
656 }
657 }
658
Adam Powell32c3a692011-01-09 21:28:43 -0800659 private int getThumbPositionForListPosition(int firstVisibleItem, int visibleItemCount,
660 int totalItemCount) {
Adam Powell35948b72011-08-25 14:15:59 -0700661 if (mSectionIndexer == null || mListAdapter == null) {
Adam Powell32c3a692011-01-09 21:28:43 -0800662 getSectionsFromIndexer();
663 }
Adam Powell568ccd82011-08-03 22:38:48 -0700664 if (mSectionIndexer == null || !mMatchDragPosition) {
Adam Powell32c3a692011-01-09 21:28:43 -0800665 return ((mList.getHeight() - mThumbH) * firstVisibleItem)
666 / (totalItemCount - visibleItemCount);
667 }
668
669 firstVisibleItem -= mListOffset;
670 if (firstVisibleItem < 0) {
671 return 0;
672 }
673 totalItemCount -= mListOffset;
674
675 final int trackHeight = mList.getHeight() - mThumbH;
676
677 final int section = mSectionIndexer.getSectionForPosition(firstVisibleItem);
678 final int sectionPos = mSectionIndexer.getPositionForSection(section);
Vairavan Srinivasan4fc2fa62013-03-04 20:34:46 -0800679 final int nextSectionPos;
Adam Powellf49971e2011-06-14 22:00:01 -0700680 final int sectionCount = mSections.length;
Vairavan Srinivasan4fc2fa62013-03-04 20:34:46 -0800681 if (section + 1 < sectionCount) {
682 nextSectionPos = mSectionIndexer.getPositionForSection(section + 1);
683 } else {
684 nextSectionPos = totalItemCount - 1;
685 }
Adam Powell32c3a692011-01-09 21:28:43 -0800686 final int positionsInSection = nextSectionPos - sectionPos;
687
688 final View child = mList.getChildAt(0);
Adam Powell32aa2c92011-01-11 15:37:04 -0800689 final float incrementalPos = child == null ? 0 : firstVisibleItem +
Adam Powell32c3a692011-01-09 21:28:43 -0800690 (float) (mList.getPaddingTop() - child.getTop()) / child.getHeight();
691 final float posWithinSection = (incrementalPos - sectionPos) / positionsInSection;
692 int result = (int) ((section + posWithinSection) / sectionCount * trackHeight);
693
694 // Fake out the scrollbar for the last item. Since the section indexer won't
695 // ever actually move the list in this end space, make scrolling across the last item
696 // account for whatever space is remaining.
697 if (firstVisibleItem > 0 && firstVisibleItem + visibleItemCount == totalItemCount) {
698 final View lastChild = mList.getChildAt(visibleItemCount - 1);
699 final float lastItemVisible = (float) (mList.getHeight() - mList.getPaddingBottom()
700 - lastChild.getTop()) / lastChild.getHeight();
701 result += (trackHeight - result) * lastItemVisible;
702 }
703
704 return result;
705 }
706
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800707 private void cancelFling() {
708 // Cancel the list fling
709 MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
710 mList.onTouchEvent(cancelFling);
711 cancelFling.recycle();
712 }
713
Adam Powellaf5280c2011-10-11 18:36:34 -0700714 void cancelPendingDrag() {
715 mList.removeCallbacks(mDeferStartDrag);
716 mPendingDrag = false;
717 }
718
719 void startPendingDrag() {
720 mPendingDrag = true;
721 mList.postDelayed(mDeferStartDrag, PENDING_DRAG_DELAY);
722 }
723
724 void beginDrag() {
725 setState(STATE_DRAGGING);
726 if (mListAdapter == null && mList != null) {
727 getSectionsFromIndexer();
728 }
729 if (mList != null) {
730 mList.requestDisallowInterceptTouchEvent(true);
731 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
732 }
733
734 cancelFling();
735 }
736
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800737 boolean onInterceptTouchEvent(MotionEvent ev) {
Adam Powellaf5280c2011-10-11 18:36:34 -0700738 switch (ev.getActionMasked()) {
739 case MotionEvent.ACTION_DOWN:
740 if (mState > STATE_NONE && isPointInside(ev.getX(), ev.getY())) {
741 if (!mList.isInScrollingContainer()) {
742 beginDrag();
743 return true;
744 }
745 mInitialTouchY = ev.getY();
746 startPendingDrag();
747 }
748 break;
749 case MotionEvent.ACTION_UP:
750 case MotionEvent.ACTION_CANCEL:
751 cancelPendingDrag();
752 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800753 }
754 return false;
755 }
756
757 boolean onTouchEvent(MotionEvent me) {
758 if (mState == STATE_NONE) {
759 return false;
760 }
Romain Guy82f34952009-05-24 18:40:45 -0700761
762 final int action = me.getAction();
763
764 if (action == MotionEvent.ACTION_DOWN) {
765 if (isPointInside(me.getX(), me.getY())) {
Adam Powellaf5280c2011-10-11 18:36:34 -0700766 if (!mList.isInScrollingContainer()) {
767 beginDrag();
768 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800769 }
Adam Powellaf5280c2011-10-11 18:36:34 -0700770 mInitialTouchY = me.getY();
771 startPendingDrag();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800772 }
Marco Nelissencd3e4ad2010-02-03 11:04:58 -0800773 } else if (action == MotionEvent.ACTION_UP) { // don't add ACTION_CANCEL here
Adam Powellaf5280c2011-10-11 18:36:34 -0700774 if (mPendingDrag) {
775 // Allow a tap to scroll.
776 beginDrag();
777
778 final int viewHeight = mList.getHeight();
779 // Jitter
780 int newThumbY = (int) me.getY() - mThumbH + 10;
781 if (newThumbY < 0) {
782 newThumbY = 0;
783 } else if (newThumbY + mThumbH > viewHeight) {
784 newThumbY = viewHeight - mThumbH;
785 }
786 mThumbY = newThumbY;
787 scrollTo((float) mThumbY / (viewHeight - mThumbH));
788
789 cancelPendingDrag();
790 // Will hit the STATE_DRAGGING check below
791 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800792 if (mState == STATE_DRAGGING) {
Marco Nelissend43fe072010-02-03 10:22:21 -0800793 if (mList != null) {
794 // ViewGroup does the right thing already, but there might
795 // be other classes that don't properly reset on touch-up,
796 // so do this explicitly just in case.
797 mList.requestDisallowInterceptTouchEvent(false);
Adam Powelld43bd482010-02-26 16:29:09 -0800798 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
Marco Nelissend43fe072010-02-03 10:22:21 -0800799 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800800 setState(STATE_VISIBLE);
801 final Handler handler = mHandler;
802 handler.removeCallbacks(mScrollFade);
Adam Powell20232d02010-12-08 21:08:53 -0800803 if (!mAlwaysShow) {
804 handler.postDelayed(mScrollFade, 1000);
805 }
806
807 mList.invalidate();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800808 return true;
809 }
Romain Guy82f34952009-05-24 18:40:45 -0700810 } else if (action == MotionEvent.ACTION_MOVE) {
Adam Powellaf5280c2011-10-11 18:36:34 -0700811 if (mPendingDrag) {
812 final float y = me.getY();
813 if (Math.abs(y - mInitialTouchY) > mScaledTouchSlop) {
814 setState(STATE_DRAGGING);
815 if (mListAdapter == null && mList != null) {
816 getSectionsFromIndexer();
817 }
818 if (mList != null) {
819 mList.requestDisallowInterceptTouchEvent(true);
820 mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
821 }
822
823 cancelFling();
824 cancelPendingDrag();
825 // Will hit the STATE_DRAGGING check below
826 }
827 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800828 if (mState == STATE_DRAGGING) {
829 final int viewHeight = mList.getHeight();
830 // Jitter
831 int newThumbY = (int) me.getY() - mThumbH + 10;
832 if (newThumbY < 0) {
833 newThumbY = 0;
834 } else if (newThumbY + mThumbH > viewHeight) {
835 newThumbY = viewHeight - mThumbH;
836 }
837 if (Math.abs(mThumbY - newThumbY) < 2) {
838 return true;
839 }
840 mThumbY = newThumbY;
841 // If the previous scrollTo is still pending
842 if (mScrollCompleted) {
843 scrollTo((float) mThumbY / (viewHeight - mThumbH));
844 }
845 return true;
846 }
Adam Powellaf5280c2011-10-11 18:36:34 -0700847 } else if (action == MotionEvent.ACTION_CANCEL) {
848 cancelPendingDrag();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800849 }
850 return false;
851 }
Romain Guyd6a463a2009-05-21 23:10:10 -0700852
Romain Guy82f34952009-05-24 18:40:45 -0700853 boolean isPointInside(float x, float y) {
Adam Powell20232d02010-12-08 21:08:53 -0800854 boolean inTrack = false;
855 switch (mPosition) {
856 default:
Adam Powell20232d02010-12-08 21:08:53 -0800857 case View.SCROLLBAR_POSITION_RIGHT:
858 inTrack = x > mList.getWidth() - mThumbW;
859 break;
860 case View.SCROLLBAR_POSITION_LEFT:
861 inTrack = x < mThumbW;
862 break;
863 }
864
865 // Allow taps in the track to start moving.
866 return inTrack && (mTrackDrawable != null || y >= mThumbY && y <= mThumbY + mThumbH);
Romain Guy82f34952009-05-24 18:40:45 -0700867 }
868
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800869 public class ScrollFade implements Runnable {
870
871 long mStartTime;
872 long mFadeDuration;
873 static final int ALPHA_MAX = 208;
874 static final long FADE_DURATION = 200;
875
876 void startFade() {
877 mFadeDuration = FADE_DURATION;
878 mStartTime = SystemClock.uptimeMillis();
879 setState(STATE_EXIT);
880 }
881
882 int getAlpha() {
883 if (getState() != STATE_EXIT) {
884 return ALPHA_MAX;
885 }
886 int alpha;
887 long now = SystemClock.uptimeMillis();
888 if (now > mStartTime + mFadeDuration) {
889 alpha = 0;
890 } else {
891 alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration);
892 }
893 return alpha;
894 }
895
896 public void run() {
897 if (getState() != STATE_EXIT) {
898 startFade();
899 return;
900 }
901
902 if (getAlpha() > 0) {
903 mList.invalidate();
904 } else {
905 setState(STATE_NONE);
906 }
907 }
908 }
909}