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