blob: f45e947372e43458724a26aaee3c4cd7d4ea7d0c [file] [log] [blame]
The Android Open Source Project5dc3b4f2008-10-21 07:00:00 -07001/*
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 com.android.contacts;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.RectF;
24import android.graphics.drawable.Drawable;
25import android.os.Handler;
26import android.os.SystemClock;
27import android.util.AttributeSet;
28import android.view.MotionEvent;
29import android.view.View;
30import android.view.ViewGroup.OnHierarchyChangeListener;
31import android.widget.AbsListView;
32import android.widget.Adapter;
33import android.widget.BaseAdapter;
34import android.widget.FrameLayout;
35import android.widget.HeaderViewListAdapter;
36import android.widget.ListView;
37import android.widget.AbsListView.OnScrollListener;
38
39/**
40 * FastScrollView is meant for embedding {@link ListView}s that contain a large number of
41 * items that can be indexed in some fashion. It displays a special scroll bar that allows jumping
42 * quickly to indexed sections of the list in touch-mode. Only one child can be added to this
43 * view group and it must be a {@link ListView}, with an adapter that is derived from
44 * {@link BaseAdapter}.
45 */
46public class FastScrollView extends FrameLayout
47 implements OnScrollListener, OnHierarchyChangeListener {
48
49 private Drawable mCurrentThumb;
50 private Drawable mOverlayDrawable;
51
52 private int mThumbH;
53 private int mThumbW;
54 private int mThumbY;
55
56 private RectF mOverlayPos;
57
58 // Hard coding these for now
59 private int mOverlaySize = 104;
60
61 private boolean mDragging;
62 private ListView mList;
63 private boolean mScrollCompleted;
64 private boolean mThumbVisible;
65 private int mVisibleItem;
66 private Paint mPaint;
67 private int mListOffset;
68
69 private Object [] mSections;
70 private String mSectionText;
71 private boolean mDrawOverlay;
72 private ScrollFade mScrollFade;
73
74 private Handler mHandler = new Handler();
75
76 private BaseAdapter mListAdapter;
77
78 private boolean mChangedBounds;
79
80 interface SectionIndexer {
81 Object[] getSections();
82
83 int getPositionForSection(int section);
84
85 int getSectionForPosition(int position);
86 }
87
88 public FastScrollView(Context context) {
89 super(context);
90
91 init(context);
92 }
93
94
95 public FastScrollView(Context context, AttributeSet attrs) {
96 super(context, attrs);
97
98 init(context);
99 }
100
101 public FastScrollView(Context context, AttributeSet attrs, int defStyle) {
102 super(context, attrs, defStyle);
103
104 init(context);
105 }
106
107 private void useThumbDrawable(Drawable drawable) {
108 mCurrentThumb = drawable;
109 mThumbW = 64; //mCurrentThumb.getIntrinsicWidth();
110 mThumbH = 52; //mCurrentThumb.getIntrinsicHeight();
111 mChangedBounds = true;
112 }
113
114 private void init(Context context) {
115 // Get both the scrollbar states drawables
116 final Resources res = context.getResources();
117 useThumbDrawable(res.getDrawable(
118 com.android.internal.R.drawable.scrollbar_handle_accelerated_anim2));
119
120 mOverlayDrawable = res.getDrawable(R.drawable.dialog_full_dark);
121
122 mScrollCompleted = true;
123 setWillNotDraw(false);
124
125 // Need to know when the ListView is added
126 setOnHierarchyChangeListener(this);
127
128 mOverlayPos = new RectF();
129 mScrollFade = new ScrollFade();
130 mPaint = new Paint();
131 mPaint.setAntiAlias(true);
132 mPaint.setTextAlign(Paint.Align.CENTER);
133 mPaint.setTextSize(mOverlaySize / 2);
134 mPaint.setColor(0xFFFFFFFF);
135 mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
136 }
137
138 private void removeThumb() {
139
140 mThumbVisible = false;
141 // Draw one last time to remove thumb
142 invalidate();
143 }
144
145 @Override
146 public void draw(Canvas canvas) {
147 super.draw(canvas);
148
149 if (!mThumbVisible) {
150 // No need to draw the rest
151 return;
152 }
153
154 final int y = mThumbY;
155 final int viewWidth = getWidth();
156 final FastScrollView.ScrollFade scrollFade = mScrollFade;
157
158 int alpha = -1;
159 if (scrollFade.mStarted) {
160 alpha = scrollFade.getAlpha();
161 if (alpha < ScrollFade.ALPHA_MAX / 2) {
162 mCurrentThumb.setAlpha(alpha * 2);
163 }
164 int left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
165 mCurrentThumb.setBounds(left, 0, viewWidth, mThumbH);
166 mChangedBounds = true;
167 }
168
169 canvas.translate(0, y);
170 mCurrentThumb.draw(canvas);
171 canvas.translate(0, -y);
172
173 // If user is dragging the scroll bar, draw the alphabet overlay
174 if (mDragging && mDrawOverlay) {
175 mOverlayDrawable.draw(canvas);
176 final Paint paint = mPaint;
177 float descent = paint.descent();
178 final RectF rectF = mOverlayPos;
179 canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2,
180 (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent, paint);
181 } else if (alpha == 0) {
182 scrollFade.mStarted = false;
183 removeThumb();
184 } else {
185 invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);
186 }
187 }
188
189 @Override
190 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
191 super.onSizeChanged(w, h, oldw, oldh);
192 if (mCurrentThumb != null) {
193 mCurrentThumb.setBounds(w - mThumbW, 0, w, mThumbH);
194 }
195 final RectF pos = mOverlayPos;
196 pos.left = (w - mOverlaySize) / 2;
197 pos.right = pos.left + mOverlaySize;
198 pos.top = h / 10; // 10% from top
199 pos.bottom = pos.top + mOverlaySize;
200 mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
201 (int) pos.right, (int) pos.bottom);
202 }
203
204 public void onScrollStateChanged(AbsListView view, int scrollState) {
205 }
206
207 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
208 int totalItemCount) {
209
210 if (totalItemCount - visibleItemCount > 0 && !mDragging) {
211 mThumbY = ((getHeight() - mThumbH) * firstVisibleItem) / (totalItemCount - visibleItemCount);
212 if (mChangedBounds) {
213 final int viewWidth = getWidth();
214 mCurrentThumb.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH);
215 mChangedBounds = false;
216 }
217 }
218 mScrollCompleted = true;
219 if (firstVisibleItem == mVisibleItem) {
220 return;
221 }
222 mVisibleItem = firstVisibleItem;
223 if (!mThumbVisible || mScrollFade.mStarted) {
224 mThumbVisible = true;
225 mCurrentThumb.setAlpha(ScrollFade.ALPHA_MAX);
226 }
227 mHandler.removeCallbacks(mScrollFade);
228 mScrollFade.mStarted = false;
229 if (!mDragging) {
230 mHandler.postDelayed(mScrollFade, 1500);
231 }
232 }
233
234
235 private void getSections() {
236 Adapter adapter = mList.getAdapter();
237 if (adapter instanceof HeaderViewListAdapter) {
238 mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
239 adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
240 }
241 if (adapter instanceof SectionIndexer) {
242 mListAdapter = (BaseAdapter) adapter;
243 mSections = ((SectionIndexer) mListAdapter).getSections();
244 }
245 }
246
247 public void onChildViewAdded(View parent, View child) {
248 if (child instanceof ListView) {
249 mList = (ListView)child;
250
251 mList.setOnScrollListener(this);
252 getSections();
253 }
254 }
255
256 public void onChildViewRemoved(View parent, View child) {
257 if (child == mList) {
258 mList = null;
259 mListAdapter = null;
260 mSections = null;
261 }
262 }
263
264 @Override
265 public boolean onInterceptTouchEvent(MotionEvent ev) {
266 if (mThumbVisible && ev.getAction() == MotionEvent.ACTION_DOWN) {
267 if (ev.getX() > getWidth() - mThumbW && ev.getY() >= mThumbY &&
268 ev.getY() <= mThumbY + mThumbH) {
269 mDragging = true;
270 return true;
271 }
272 }
273 return false;
274 }
275
276 private void scrollTo(float position) {
277 int count = mList.getCount();
278 mScrollCompleted = false;
279 final Object[] sections = mSections;
280 int sectionIndex;
281 if (sections != null && sections.length > 1) {
282 final int nSections = sections.length;
283 int section = (int) (position * nSections);
284 if (section >= nSections) {
285 section = nSections - 1;
286 }
287 sectionIndex = section;
288 final SectionIndexer baseAdapter = (SectionIndexer) mListAdapter;
289 int index = baseAdapter.getPositionForSection(section);
290
291 // Given the expected section and index, the following code will
292 // try to account for missing sections (no names starting with..)
293 // It will compute the scroll space of surrounding empty sections
294 // and interpolate the currently visible letter's range across the
295 // available space, so that there is always some list movement while
296 // the user moves the thumb.
297 int nextIndex = count;
298 int prevIndex = index;
299 int prevSection = section;
300 int nextSection = section + 1;
301 // Assume the next section is unique
302 if (section < nSections - 1) {
303 nextIndex = baseAdapter.getPositionForSection(section + 1);
304 }
305
306 // Find the previous index if we're slicing the previous section
307 if (nextIndex == index) {
308 // Non-existent letter
309 while (section > 0) {
310 section--;
311 prevIndex = baseAdapter.getPositionForSection(section);
312 if (prevIndex != index) {
313 prevSection = section;
314 sectionIndex = section;
315 break;
316 }
317 }
318 }
319 // Find the next index, in case the assumed next index is not
320 // unique. For instance, if there is no P, then request for P's
321 // position actually returns Q's. So we need to look ahead to make
322 // sure that there is really a Q at Q's position. If not, move
323 // further down...
324 int nextNextSection = nextSection + 1;
325 while (nextNextSection < nSections &&
326 baseAdapter.getPositionForSection(nextNextSection) == nextIndex) {
327 nextNextSection++;
328 nextSection++;
329 }
330 // Compute the beginning and ending scroll range percentage of the
331 // currently visible letter. This could be equal to or greater than
332 // (1 / nSections).
333 float fPrev = (float) prevSection / nSections;
334 float fNext = (float) nextSection / nSections;
335 index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev)
336 / (fNext - fPrev));
337 // Don't overflow
338 if (index > count - 1) index = count - 1;
339
340 mList.setSelectionFromTop(index + mListOffset, 0);
341 } else {
342 int index = (int) (position * count);
343 mList.setSelectionFromTop(index + mListOffset, 0);
344 sectionIndex = -1;
345 }
346
347 if (sectionIndex >= 0) {
348 String text = mSectionText = sections[sectionIndex].toString();
349 mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') &&
350 sectionIndex < sections.length;
351 } else {
352 mDrawOverlay = false;
353 }
354 }
355
356 private void cancelFling() {
357 // Cancel the list fling
358 MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
359 mList.onTouchEvent(cancelFling);
360 cancelFling.recycle();
361 }
362
363 @Override
364 public boolean onTouchEvent(MotionEvent me) {
365 if (me.getAction() == MotionEvent.ACTION_DOWN) {
366 if (me.getX() > getWidth() - mThumbW
367 && me.getY() >= mThumbY
368 && me.getY() <= mThumbY + mThumbH) {
369
370 mDragging = true;
371 if (mListAdapter == null && mList != null) {
372 getSections();
373 }
374
375 cancelFling();
376 return true;
377 }
378 } else if (me.getAction() == MotionEvent.ACTION_UP) {
379 if (mDragging) {
380 mDragging = false;
381 final Handler handler = mHandler;
382 handler.removeCallbacks(mScrollFade);
383 handler.postDelayed(mScrollFade, 1000);
384 return true;
385 }
386 } else if (me.getAction() == MotionEvent.ACTION_MOVE) {
387 if (mDragging) {
388 final int viewHeight = getHeight();
389 mThumbY = (int) me.getY() - mThumbH + 10;
390 if (mThumbY < 0) {
391 mThumbY = 0;
392 } else if (mThumbY + mThumbH > viewHeight) {
393 mThumbY = viewHeight - mThumbH;
394 }
395 // If the previous scrollTo is still pending
396 if (mScrollCompleted) {
397 scrollTo((float) mThumbY / (viewHeight - mThumbH));
398 }
399 return true;
400 }
401 }
402
403 return super.onTouchEvent(me);
404 }
405
406 public class ScrollFade implements Runnable {
407
408 long mStartTime;
409 long mFadeDuration;
410 boolean mStarted;
411 static final int ALPHA_MAX = 255;
412 static final long FADE_DURATION = 200;
413
414 void startFade() {
415 mFadeDuration = FADE_DURATION;
416 mStartTime = SystemClock.uptimeMillis();
417 mStarted = true;
418 }
419
420 int getAlpha() {
421 if (!mStarted) {
422 return ALPHA_MAX;
423 }
424 int alpha;
425 long now = SystemClock.uptimeMillis();
426 if (now > mStartTime + mFadeDuration) {
427 alpha = 0;
428 } else {
429 alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration);
430 }
431 return alpha;
432 }
433
434 public void run() {
435 if (!mStarted) {
436 startFade();
437 invalidate();
438 }
439
440 if (getAlpha() > 0) {
441 final int y = mThumbY;
442 final int viewWidth = getWidth();
443 invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);
444 } else {
445 mStarted = false;
446 removeThumb();
447 }
448 }
449 }
450}