/CVox/src/com/android/contacts/FastScrollView.java

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