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 android.widget; 18 19 import android.content.Context; 20 import android.content.res.ColorStateList; 21 import android.content.res.Resources; 22 import android.content.res.TypedArray; 23 import android.graphics.Canvas; 24 import android.graphics.Paint; 25 import android.graphics.RectF; 26 import android.graphics.drawable.Drawable; 27 import android.os.Handler; 28 import android.os.SystemClock; 29 import android.util.TypedValue; 30 import android.view.MotionEvent; 31 32 /** 33 * Helper class for AbsListView to draw and control the Fast Scroll thumb 34 */ 35 class FastScroller { 36 37 // Minimum number of pages to justify showing a fast scroll thumb 38 private static int MIN_PAGES = 4; 39 // Scroll thumb not showing 40 private static final int STATE_NONE = 0; 41 // Not implemented yet - fade-in transition 42 private static final int STATE_ENTER = 1; 43 // Scroll thumb visible and moving along with the scrollbar 44 private static final int STATE_VISIBLE = 2; 45 // Scroll thumb being dragged by user 46 private static final int STATE_DRAGGING = 3; 47 // Scroll thumb fading out due to inactivity timeout 48 private static final int STATE_EXIT = 4; 49 50 private Drawable mThumbDrawable; 51 private Drawable mOverlayDrawable; 52 53 private int mThumbH; 54 private int mThumbW; 55 private int mThumbY; 56 57 private RectF mOverlayPos; 58 private int mOverlaySize; 59 60 private AbsListView mList; 61 private boolean mScrollCompleted; 62 private int mVisibleItem; 63 private Paint mPaint; 64 private int mListOffset; 65 private int mItemCount = -1; 66 private boolean mLongList; 67 68 private Object [] mSections; 69 private String mSectionText; 70 private boolean mDrawOverlay; 71 private ScrollFade mScrollFade; 72 73 private int mState; 74 75 private Handler mHandler = new Handler(); 76 77 private BaseAdapter mListAdapter; 78 private SectionIndexer mSectionIndexer; 79 80 private boolean mChangedBounds; 81 FastScroller(Context context, AbsListView listView)82 public FastScroller(Context context, AbsListView listView) { 83 mList = listView; 84 init(context); 85 } 86 setState(int state)87 public void setState(int state) { 88 switch (state) { 89 case STATE_NONE: 90 mHandler.removeCallbacks(mScrollFade); 91 mList.invalidate(); 92 break; 93 case STATE_VISIBLE: 94 if (mState != STATE_VISIBLE) { // Optimization 95 resetThumbPos(); 96 } 97 // Fall through 98 case STATE_DRAGGING: 99 mHandler.removeCallbacks(mScrollFade); 100 break; 101 case STATE_EXIT: 102 int viewWidth = mList.getWidth(); 103 mList.invalidate(viewWidth - mThumbW, mThumbY, viewWidth, mThumbY + mThumbH); 104 break; 105 } 106 mState = state; 107 } 108 getState()109 public int getState() { 110 return mState; 111 } 112 resetThumbPos()113 private void resetThumbPos() { 114 final int viewWidth = mList.getWidth(); 115 // Bounds are always top right. Y coordinate get's translated during draw 116 mThumbDrawable.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH); 117 mThumbDrawable.setAlpha(ScrollFade.ALPHA_MAX); 118 } 119 useThumbDrawable(Context context, Drawable drawable)120 private void useThumbDrawable(Context context, Drawable drawable) { 121 mThumbDrawable = drawable; 122 mThumbW = context.getResources().getDimensionPixelSize( 123 com.android.internal.R.dimen.fastscroll_thumb_width); 124 mThumbH = context.getResources().getDimensionPixelSize( 125 com.android.internal.R.dimen.fastscroll_thumb_height); 126 mChangedBounds = true; 127 } 128 init(Context context)129 private void init(Context context) { 130 // Get both the scrollbar states drawables 131 final Resources res = context.getResources(); 132 useThumbDrawable(context, res.getDrawable( 133 com.android.internal.R.drawable.scrollbar_handle_accelerated_anim2)); 134 135 mOverlayDrawable = res.getDrawable( 136 com.android.internal.R.drawable.menu_submenu_background); 137 138 mScrollCompleted = true; 139 140 getSectionsFromIndexer(); 141 142 mOverlaySize = context.getResources().getDimensionPixelSize( 143 com.android.internal.R.dimen.fastscroll_overlay_size); 144 mOverlayPos = new RectF(); 145 mScrollFade = new ScrollFade(); 146 mPaint = new Paint(); 147 mPaint.setAntiAlias(true); 148 mPaint.setTextAlign(Paint.Align.CENTER); 149 mPaint.setTextSize(mOverlaySize / 2); 150 TypedArray ta = context.getTheme().obtainStyledAttributes(new int[] { 151 android.R.attr.textColorPrimary }); 152 ColorStateList textColor = ta.getColorStateList(ta.getIndex(0)); 153 int textColorNormal = textColor.getDefaultColor(); 154 mPaint.setColor(textColorNormal); 155 mPaint.setStyle(Paint.Style.FILL_AND_STROKE); 156 157 mState = STATE_NONE; 158 } 159 stop()160 void stop() { 161 setState(STATE_NONE); 162 } 163 isVisible()164 boolean isVisible() { 165 return !(mState == STATE_NONE); 166 } 167 draw(Canvas canvas)168 public void draw(Canvas canvas) { 169 170 if (mState == STATE_NONE) { 171 // No need to draw anything 172 return; 173 } 174 175 final int y = mThumbY; 176 final int viewWidth = mList.getWidth(); 177 final FastScroller.ScrollFade scrollFade = mScrollFade; 178 179 int alpha = -1; 180 if (mState == STATE_EXIT) { 181 alpha = scrollFade.getAlpha(); 182 if (alpha < ScrollFade.ALPHA_MAX / 2) { 183 mThumbDrawable.setAlpha(alpha * 2); 184 } 185 int left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX; 186 mThumbDrawable.setBounds(left, 0, viewWidth, mThumbH); 187 mChangedBounds = true; 188 } 189 190 canvas.translate(0, y); 191 mThumbDrawable.draw(canvas); 192 canvas.translate(0, -y); 193 194 // If user is dragging the scroll bar, draw the alphabet overlay 195 if (mState == STATE_DRAGGING && mDrawOverlay) { 196 mOverlayDrawable.draw(canvas); 197 final Paint paint = mPaint; 198 float descent = paint.descent(); 199 final RectF rectF = mOverlayPos; 200 canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2, 201 (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent, paint); 202 } else if (mState == STATE_EXIT) { 203 if (alpha == 0) { // Done with exit 204 setState(STATE_NONE); 205 } else { 206 mList.invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH); 207 } 208 } 209 } 210 onSizeChanged(int w, int h, int oldw, int oldh)211 void onSizeChanged(int w, int h, int oldw, int oldh) { 212 if (mThumbDrawable != null) { 213 mThumbDrawable.setBounds(w - mThumbW, 0, w, mThumbH); 214 } 215 final RectF pos = mOverlayPos; 216 pos.left = (w - mOverlaySize) / 2; 217 pos.right = pos.left + mOverlaySize; 218 pos.top = h / 10; // 10% from top 219 pos.bottom = pos.top + mOverlaySize; 220 if (mOverlayDrawable != null) { 221 mOverlayDrawable.setBounds((int) pos.left, (int) pos.top, 222 (int) pos.right, (int) pos.bottom); 223 } 224 } 225 onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)226 void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 227 int totalItemCount) { 228 // Are there enough pages to require fast scroll? Recompute only if total count changes 229 if (mItemCount != totalItemCount && visibleItemCount > 0) { 230 mItemCount = totalItemCount; 231 mLongList = mItemCount / visibleItemCount >= MIN_PAGES; 232 } 233 if (!mLongList) { 234 if (mState != STATE_NONE) { 235 setState(STATE_NONE); 236 } 237 return; 238 } 239 if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING ) { 240 mThumbY = ((mList.getHeight() - mThumbH) * firstVisibleItem) 241 / (totalItemCount - visibleItemCount); 242 if (mChangedBounds) { 243 resetThumbPos(); 244 mChangedBounds = false; 245 } 246 } 247 mScrollCompleted = true; 248 if (firstVisibleItem == mVisibleItem) { 249 return; 250 } 251 mVisibleItem = firstVisibleItem; 252 if (mState != STATE_DRAGGING) { 253 setState(STATE_VISIBLE); 254 mHandler.postDelayed(mScrollFade, 1500); 255 } 256 } 257 getSectionIndexer()258 SectionIndexer getSectionIndexer() { 259 return mSectionIndexer; 260 } 261 getSections()262 Object[] getSections() { 263 if (mListAdapter == null && mList != null) { 264 getSectionsFromIndexer(); 265 } 266 return mSections; 267 } 268 getSectionsFromIndexer()269 private void getSectionsFromIndexer() { 270 Adapter adapter = mList.getAdapter(); 271 mSectionIndexer = null; 272 if (adapter instanceof HeaderViewListAdapter) { 273 mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount(); 274 adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter(); 275 } 276 if (adapter instanceof ExpandableListConnector) { 277 ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter(); 278 if (expAdapter instanceof SectionIndexer) { 279 mSectionIndexer = (SectionIndexer) expAdapter; 280 mListAdapter = (BaseAdapter) adapter; 281 mSections = mSectionIndexer.getSections(); 282 } 283 } else { 284 if (adapter instanceof SectionIndexer) { 285 mListAdapter = (BaseAdapter) adapter; 286 mSectionIndexer = (SectionIndexer) adapter; 287 mSections = mSectionIndexer.getSections(); 288 289 } else { 290 mListAdapter = (BaseAdapter) adapter; 291 mSections = new String[] { " " }; 292 } 293 } 294 } 295 scrollTo(float position)296 private void scrollTo(float position) { 297 int count = mList.getCount(); 298 mScrollCompleted = false; 299 float fThreshold = (1.0f / count) / 8; 300 final Object[] sections = mSections; 301 int sectionIndex; 302 if (sections != null && sections.length > 1) { 303 final int nSections = sections.length; 304 int section = (int) (position * nSections); 305 if (section >= nSections) { 306 section = nSections - 1; 307 } 308 int exactSection = section; 309 sectionIndex = section; 310 int index = mSectionIndexer.getPositionForSection(section); 311 // Given the expected section and index, the following code will 312 // try to account for missing sections (no names starting with..) 313 // It will compute the scroll space of surrounding empty sections 314 // and interpolate the currently visible letter's range across the 315 // available space, so that there is always some list movement while 316 // the user moves the thumb. 317 int nextIndex = count; 318 int prevIndex = index; 319 int prevSection = section; 320 int nextSection = section + 1; 321 // Assume the next section is unique 322 if (section < nSections - 1) { 323 nextIndex = mSectionIndexer.getPositionForSection(section + 1); 324 } 325 326 // Find the previous index if we're slicing the previous section 327 if (nextIndex == index) { 328 // Non-existent letter 329 while (section > 0) { 330 section--; 331 prevIndex = mSectionIndexer.getPositionForSection(section); 332 if (prevIndex != index) { 333 prevSection = section; 334 sectionIndex = section; 335 break; 336 } else if (section == 0) { 337 // When section reaches 0 here, sectionIndex must follow it. 338 // Assuming mSectionIndexer.getPositionForSection(0) == 0. 339 sectionIndex = 0; 340 break; 341 } 342 } 343 } 344 // Find the next index, in case the assumed next index is not 345 // unique. For instance, if there is no P, then request for P's 346 // position actually returns Q's. So we need to look ahead to make 347 // sure that there is really a Q at Q's position. If not, move 348 // further down... 349 int nextNextSection = nextSection + 1; 350 while (nextNextSection < nSections && 351 mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) { 352 nextNextSection++; 353 nextSection++; 354 } 355 // Compute the beginning and ending scroll range percentage of the 356 // currently visible letter. This could be equal to or greater than 357 // (1 / nSections). 358 float fPrev = (float) prevSection / nSections; 359 float fNext = (float) nextSection / nSections; 360 if (prevSection == exactSection && position - fPrev < fThreshold) { 361 index = prevIndex; 362 } else { 363 index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev) 364 / (fNext - fPrev)); 365 } 366 // Don't overflow 367 if (index > count - 1) index = count - 1; 368 369 if (mList instanceof ExpandableListView) { 370 ExpandableListView expList = (ExpandableListView) mList; 371 expList.setSelectionFromTop(expList.getFlatListPosition( 372 ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0); 373 } else if (mList instanceof ListView) { 374 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0); 375 } else { 376 mList.setSelection(index + mListOffset); 377 } 378 } else { 379 int index = (int) (position * count); 380 if (mList instanceof ExpandableListView) { 381 ExpandableListView expList = (ExpandableListView) mList; 382 expList.setSelectionFromTop(expList.getFlatListPosition( 383 ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0); 384 } else if (mList instanceof ListView) { 385 ((ListView)mList).setSelectionFromTop(index + mListOffset, 0); 386 } else { 387 mList.setSelection(index + mListOffset); 388 } 389 sectionIndex = -1; 390 } 391 392 if (sectionIndex >= 0) { 393 String text = mSectionText = sections[sectionIndex].toString(); 394 mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') && 395 sectionIndex < sections.length; 396 } else { 397 mDrawOverlay = false; 398 } 399 } 400 401 private void cancelFling() { 402 // Cancel the list fling 403 MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0); 404 mList.onTouchEvent(cancelFling); 405 cancelFling.recycle(); 406 } 407 408 boolean onInterceptTouchEvent(MotionEvent ev) { 409 if (mState > STATE_NONE && ev.getAction() == MotionEvent.ACTION_DOWN) { 410 if (isPointInside(ev.getX(), ev.getY())) { 411 setState(STATE_DRAGGING); 412 return true; 413 } 414 } 415 return false; 416 } 417 onTouchEvent(MotionEvent me)418 boolean onTouchEvent(MotionEvent me) { 419 if (mState == STATE_NONE) { 420 return false; 421 } 422 423 final int action = me.getAction(); 424 425 if (action == MotionEvent.ACTION_DOWN) { 426 if (isPointInside(me.getX(), me.getY())) { 427 setState(STATE_DRAGGING); 428 if (mListAdapter == null && mList != null) { 429 getSectionsFromIndexer(); 430 } 431 432 cancelFling(); 433 return true; 434 } 435 } else if (action == MotionEvent.ACTION_UP) { 436 if (mState == STATE_DRAGGING) { 437 setState(STATE_VISIBLE); 438 final Handler handler = mHandler; 439 handler.removeCallbacks(mScrollFade); 440 handler.postDelayed(mScrollFade, 1000); 441 return true; 442 } 443 } else if (action == MotionEvent.ACTION_MOVE) { 444 if (mState == STATE_DRAGGING) { 445 final int viewHeight = mList.getHeight(); 446 // Jitter 447 int newThumbY = (int) me.getY() - mThumbH + 10; 448 if (newThumbY < 0) { 449 newThumbY = 0; 450 } else if (newThumbY + mThumbH > viewHeight) { 451 newThumbY = viewHeight - mThumbH; 452 } 453 if (Math.abs(mThumbY - newThumbY) < 2) { 454 return true; 455 } 456 mThumbY = newThumbY; 457 // If the previous scrollTo is still pending 458 if (mScrollCompleted) { 459 scrollTo((float) mThumbY / (viewHeight - mThumbH)); 460 } 461 return true; 462 } 463 } 464 return false; 465 } 466 isPointInside(float x, float y)467 boolean isPointInside(float x, float y) { 468 return x > mList.getWidth() - mThumbW && y >= mThumbY && y <= mThumbY + mThumbH; 469 } 470 471 public class ScrollFade implements Runnable { 472 473 long mStartTime; 474 long mFadeDuration; 475 static final int ALPHA_MAX = 208; 476 static final long FADE_DURATION = 200; 477 startFade()478 void startFade() { 479 mFadeDuration = FADE_DURATION; 480 mStartTime = SystemClock.uptimeMillis(); 481 setState(STATE_EXIT); 482 } 483 getAlpha()484 int getAlpha() { 485 if (getState() != STATE_EXIT) { 486 return ALPHA_MAX; 487 } 488 int alpha; 489 long now = SystemClock.uptimeMillis(); 490 if (now > mStartTime + mFadeDuration) { 491 alpha = 0; 492 } else { 493 alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration); 494 } 495 return alpha; 496 } 497 run()498 public void run() { 499 if (getState() != STATE_EXIT) { 500 startFade(); 501 return; 502 } 503 504 if (getAlpha() > 0) { 505 mList.invalidate(); 506 } else { 507 setState(STATE_NONE); 508 } 509 } 510 } 511 } 512