Initial Contribution
diff --git a/src/com/android/contacts/FastScrollView.java b/src/com/android/contacts/FastScrollView.java
new file mode 100644
index 0000000..f45e947
--- /dev/null
+++ b/src/com/android/contacts/FastScrollView.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.OnHierarchyChangeListener;
+import android.widget.AbsListView;
+import android.widget.Adapter;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.HeaderViewListAdapter;
+import android.widget.ListView;
+import android.widget.AbsListView.OnScrollListener;
+
+/**
+ * FastScrollView is meant for embedding {@link ListView}s that contain a large number of
+ * items that can be indexed in some fashion. It displays a special scroll bar that allows jumping
+ * quickly to indexed sections of the list in touch-mode. Only one child can be added to this
+ * view group and it must be a {@link ListView}, with an adapter that is derived from
+ * {@link BaseAdapter}.
+ */
+public class FastScrollView extends FrameLayout
+ implements OnScrollListener, OnHierarchyChangeListener {
+
+ private Drawable mCurrentThumb;
+ private Drawable mOverlayDrawable;
+
+ private int mThumbH;
+ private int mThumbW;
+ private int mThumbY;
+
+ private RectF mOverlayPos;
+
+ // Hard coding these for now
+ private int mOverlaySize = 104;
+
+ private boolean mDragging;
+ private ListView mList;
+ private boolean mScrollCompleted;
+ private boolean mThumbVisible;
+ private int mVisibleItem;
+ private Paint mPaint;
+ private int mListOffset;
+
+ private Object [] mSections;
+ private String mSectionText;
+ private boolean mDrawOverlay;
+ private ScrollFade mScrollFade;
+
+ private Handler mHandler = new Handler();
+
+ private BaseAdapter mListAdapter;
+
+ private boolean mChangedBounds;
+
+ interface SectionIndexer {
+ Object[] getSections();
+
+ int getPositionForSection(int section);
+
+ int getSectionForPosition(int position);
+ }
+
+ public FastScrollView(Context context) {
+ super(context);
+
+ init(context);
+ }
+
+
+ public FastScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ init(context);
+ }
+
+ public FastScrollView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ init(context);
+ }
+
+ private void useThumbDrawable(Drawable drawable) {
+ mCurrentThumb = drawable;
+ mThumbW = 64; //mCurrentThumb.getIntrinsicWidth();
+ mThumbH = 52; //mCurrentThumb.getIntrinsicHeight();
+ mChangedBounds = true;
+ }
+
+ private void init(Context context) {
+ // Get both the scrollbar states drawables
+ final Resources res = context.getResources();
+ useThumbDrawable(res.getDrawable(
+ com.android.internal.R.drawable.scrollbar_handle_accelerated_anim2));
+
+ mOverlayDrawable = res.getDrawable(R.drawable.dialog_full_dark);
+
+ mScrollCompleted = true;
+ setWillNotDraw(false);
+
+ // Need to know when the ListView is added
+ setOnHierarchyChangeListener(this);
+
+ mOverlayPos = new RectF();
+ mScrollFade = new ScrollFade();
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setTextAlign(Paint.Align.CENTER);
+ mPaint.setTextSize(mOverlaySize / 2);
+ mPaint.setColor(0xFFFFFFFF);
+ mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ }
+
+ private void removeThumb() {
+
+ mThumbVisible = false;
+ // Draw one last time to remove thumb
+ invalidate();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (!mThumbVisible) {
+ // No need to draw the rest
+ return;
+ }
+
+ final int y = mThumbY;
+ final int viewWidth = getWidth();
+ final FastScrollView.ScrollFade scrollFade = mScrollFade;
+
+ int alpha = -1;
+ if (scrollFade.mStarted) {
+ alpha = scrollFade.getAlpha();
+ if (alpha < ScrollFade.ALPHA_MAX / 2) {
+ mCurrentThumb.setAlpha(alpha * 2);
+ }
+ int left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
+ mCurrentThumb.setBounds(left, 0, viewWidth, mThumbH);
+ mChangedBounds = true;
+ }
+
+ canvas.translate(0, y);
+ mCurrentThumb.draw(canvas);
+ canvas.translate(0, -y);
+
+ // If user is dragging the scroll bar, draw the alphabet overlay
+ if (mDragging && mDrawOverlay) {
+ mOverlayDrawable.draw(canvas);
+ final Paint paint = mPaint;
+ float descent = paint.descent();
+ final RectF rectF = mOverlayPos;
+ canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2,
+ (int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 4 - descent, paint);
+ } else if (alpha == 0) {
+ scrollFade.mStarted = false;
+ removeThumb();
+ } else {
+ invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ if (mCurrentThumb != null) {
+ mCurrentThumb.setBounds(w - mThumbW, 0, w, mThumbH);
+ }
+ final RectF pos = mOverlayPos;
+ pos.left = (w - mOverlaySize) / 2;
+ pos.right = pos.left + mOverlaySize;
+ pos.top = h / 10; // 10% from top
+ pos.bottom = pos.top + mOverlaySize;
+ mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
+ (int) pos.right, (int) pos.bottom);
+ }
+
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ }
+
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+
+ if (totalItemCount - visibleItemCount > 0 && !mDragging) {
+ mThumbY = ((getHeight() - mThumbH) * firstVisibleItem) / (totalItemCount - visibleItemCount);
+ if (mChangedBounds) {
+ final int viewWidth = getWidth();
+ mCurrentThumb.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH);
+ mChangedBounds = false;
+ }
+ }
+ mScrollCompleted = true;
+ if (firstVisibleItem == mVisibleItem) {
+ return;
+ }
+ mVisibleItem = firstVisibleItem;
+ if (!mThumbVisible || mScrollFade.mStarted) {
+ mThumbVisible = true;
+ mCurrentThumb.setAlpha(ScrollFade.ALPHA_MAX);
+ }
+ mHandler.removeCallbacks(mScrollFade);
+ mScrollFade.mStarted = false;
+ if (!mDragging) {
+ mHandler.postDelayed(mScrollFade, 1500);
+ }
+ }
+
+
+ private void getSections() {
+ Adapter adapter = mList.getAdapter();
+ if (adapter instanceof HeaderViewListAdapter) {
+ mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
+ adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
+ }
+ if (adapter instanceof SectionIndexer) {
+ mListAdapter = (BaseAdapter) adapter;
+ mSections = ((SectionIndexer) mListAdapter).getSections();
+ }
+ }
+
+ public void onChildViewAdded(View parent, View child) {
+ if (child instanceof ListView) {
+ mList = (ListView)child;
+
+ mList.setOnScrollListener(this);
+ getSections();
+ }
+ }
+
+ public void onChildViewRemoved(View parent, View child) {
+ if (child == mList) {
+ mList = null;
+ mListAdapter = null;
+ mSections = null;
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (mThumbVisible && ev.getAction() == MotionEvent.ACTION_DOWN) {
+ if (ev.getX() > getWidth() - mThumbW && ev.getY() >= mThumbY &&
+ ev.getY() <= mThumbY + mThumbH) {
+ mDragging = true;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void scrollTo(float position) {
+ int count = mList.getCount();
+ mScrollCompleted = false;
+ final Object[] sections = mSections;
+ int sectionIndex;
+ if (sections != null && sections.length > 1) {
+ final int nSections = sections.length;
+ int section = (int) (position * nSections);
+ if (section >= nSections) {
+ section = nSections - 1;
+ }
+ sectionIndex = section;
+ final SectionIndexer baseAdapter = (SectionIndexer) mListAdapter;
+ int index = baseAdapter.getPositionForSection(section);
+
+ // Given the expected section and index, the following code will
+ // try to account for missing sections (no names starting with..)
+ // It will compute the scroll space of surrounding empty sections
+ // and interpolate the currently visible letter's range across the
+ // available space, so that there is always some list movement while
+ // the user moves the thumb.
+ int nextIndex = count;
+ int prevIndex = index;
+ int prevSection = section;
+ int nextSection = section + 1;
+ // Assume the next section is unique
+ if (section < nSections - 1) {
+ nextIndex = baseAdapter.getPositionForSection(section + 1);
+ }
+
+ // Find the previous index if we're slicing the previous section
+ if (nextIndex == index) {
+ // Non-existent letter
+ while (section > 0) {
+ section--;
+ prevIndex = baseAdapter.getPositionForSection(section);
+ if (prevIndex != index) {
+ prevSection = section;
+ sectionIndex = section;
+ break;
+ }
+ }
+ }
+ // Find the next index, in case the assumed next index is not
+ // unique. For instance, if there is no P, then request for P's
+ // position actually returns Q's. So we need to look ahead to make
+ // sure that there is really a Q at Q's position. If not, move
+ // further down...
+ int nextNextSection = nextSection + 1;
+ while (nextNextSection < nSections &&
+ baseAdapter.getPositionForSection(nextNextSection) == nextIndex) {
+ nextNextSection++;
+ nextSection++;
+ }
+ // Compute the beginning and ending scroll range percentage of the
+ // currently visible letter. This could be equal to or greater than
+ // (1 / nSections).
+ float fPrev = (float) prevSection / nSections;
+ float fNext = (float) nextSection / nSections;
+ index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev)
+ / (fNext - fPrev));
+ // Don't overflow
+ if (index > count - 1) index = count - 1;
+
+ mList.setSelectionFromTop(index + mListOffset, 0);
+ } else {
+ int index = (int) (position * count);
+ mList.setSelectionFromTop(index + mListOffset, 0);
+ sectionIndex = -1;
+ }
+
+ if (sectionIndex >= 0) {
+ String text = mSectionText = sections[sectionIndex].toString();
+ mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') &&
+ sectionIndex < sections.length;
+ } else {
+ mDrawOverlay = false;
+ }
+ }
+
+ private void cancelFling() {
+ // Cancel the list fling
+ MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mList.onTouchEvent(cancelFling);
+ cancelFling.recycle();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent me) {
+ if (me.getAction() == MotionEvent.ACTION_DOWN) {
+ if (me.getX() > getWidth() - mThumbW
+ && me.getY() >= mThumbY
+ && me.getY() <= mThumbY + mThumbH) {
+
+ mDragging = true;
+ if (mListAdapter == null && mList != null) {
+ getSections();
+ }
+
+ cancelFling();
+ return true;
+ }
+ } else if (me.getAction() == MotionEvent.ACTION_UP) {
+ if (mDragging) {
+ mDragging = false;
+ final Handler handler = mHandler;
+ handler.removeCallbacks(mScrollFade);
+ handler.postDelayed(mScrollFade, 1000);
+ return true;
+ }
+ } else if (me.getAction() == MotionEvent.ACTION_MOVE) {
+ if (mDragging) {
+ final int viewHeight = getHeight();
+ mThumbY = (int) me.getY() - mThumbH + 10;
+ if (mThumbY < 0) {
+ mThumbY = 0;
+ } else if (mThumbY + mThumbH > viewHeight) {
+ mThumbY = viewHeight - mThumbH;
+ }
+ // If the previous scrollTo is still pending
+ if (mScrollCompleted) {
+ scrollTo((float) mThumbY / (viewHeight - mThumbH));
+ }
+ return true;
+ }
+ }
+
+ return super.onTouchEvent(me);
+ }
+
+ public class ScrollFade implements Runnable {
+
+ long mStartTime;
+ long mFadeDuration;
+ boolean mStarted;
+ static final int ALPHA_MAX = 255;
+ static final long FADE_DURATION = 200;
+
+ void startFade() {
+ mFadeDuration = FADE_DURATION;
+ mStartTime = SystemClock.uptimeMillis();
+ mStarted = true;
+ }
+
+ int getAlpha() {
+ if (!mStarted) {
+ return ALPHA_MAX;
+ }
+ int alpha;
+ long now = SystemClock.uptimeMillis();
+ if (now > mStartTime + mFadeDuration) {
+ alpha = 0;
+ } else {
+ alpha = (int) (ALPHA_MAX - ((now - mStartTime) * ALPHA_MAX) / mFadeDuration);
+ }
+ return alpha;
+ }
+
+ public void run() {
+ if (!mStarted) {
+ startFade();
+ invalidate();
+ }
+
+ if (getAlpha() > 0) {
+ final int y = mThumbY;
+ final int viewWidth = getWidth();
+ invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);
+ } else {
+ mStarted = false;
+ removeThumb();
+ }
+ }
+ }
+}