// Created by plusminus on 19:03:37 - 02.12.2008 package org.androad.ui.common.views; import org.androad.R; 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 final 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 final Handler mHandler = new Handler(); private BaseAdapter mListAdapter; private boolean mChangedBounds; public interface SectionIndexer { Object[] getSections(); int getPositionForSection(int section); int getSectionForPosition(int position); } public FastScrollView(final Context context) { super(context); init(context); } public FastScrollView(final Context context, final AttributeSet attrs) { super(context, attrs); init(context); } public FastScrollView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); init(context); } private void useThumbDrawable(final Drawable drawable) { this.mCurrentThumb = drawable; this.mThumbW = 64; //mCurrentThumb.getIntrinsicWidth(); this.mThumbH = 52; //mCurrentThumb.getIntrinsicHeight(); this.mChangedBounds = true; } private void init(final Context context) { // Get both the scrollbar states drawables final Resources res = context.getResources(); useThumbDrawable(res.getDrawable(R.drawable.scrollbar_handle_accelerated_anim2)); this.mOverlayDrawable = res.getDrawable(R.drawable.dialog_full_dark); this.mScrollCompleted = true; setWillNotDraw(false); // Need to know when the ListView is added setOnHierarchyChangeListener(this); this.mOverlayPos = new RectF(); this.mScrollFade = new ScrollFade(); this.mPaint = new Paint(); this.mPaint.setAntiAlias(true); this.mPaint.setTextAlign(Paint.Align.CENTER); this.mPaint.setTextSize(this.mOverlaySize / 2); this.mPaint.setColor(0xFFFFFFFF); this.mPaint.setStyle(Paint.Style.FILL_AND_STROKE); } private void removeThumb() { this.mThumbVisible = false; // Draw one last time to remove thumb invalidate(); } @Override public void draw(final Canvas canvas) { super.draw(canvas); if (!this.mThumbVisible) { // No need to draw the rest return; } final int y = this.mThumbY; final int viewWidth = getWidth(); final FastScrollView.ScrollFade scrollFade = this.mScrollFade; int alpha = -1; if (scrollFade.mStarted) { alpha = scrollFade.getAlpha(); if (alpha < ScrollFade.ALPHA_MAX / 2) { this.mCurrentThumb.setAlpha(alpha * 2); } final int left = viewWidth - (this.mThumbW * alpha) / ScrollFade.ALPHA_MAX; this.mCurrentThumb.setBounds(left, 0, viewWidth, this.mThumbH); this.mChangedBounds = true; } canvas.translate(0, y); this.mCurrentThumb.draw(canvas); canvas.translate(0, -y); // If user is dragging the scroll bar, draw the alphabet overlay if (this.mDragging && this.mDrawOverlay) { this.mOverlayDrawable.draw(canvas); final Paint paint = this.mPaint; final float descent = paint.descent(); final RectF rectF = this.mOverlayPos; canvas.drawText(this.mSectionText, (int) (rectF.left + rectF.right) / 2, (int) (rectF.bottom + rectF.top) / 2 + this.mOverlaySize / 4 - descent, paint); } else if (alpha == 0) { scrollFade.mStarted = false; removeThumb(); } else { invalidate(viewWidth - this.mThumbW, y, viewWidth, y + this.mThumbH); } } @Override protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (this.mCurrentThumb != null) { this.mCurrentThumb.setBounds(w - this.mThumbW, 0, w, this.mThumbH); } final RectF pos = this.mOverlayPos; pos.left = (w - this.mOverlaySize) / 2; pos.right = pos.left + this.mOverlaySize; pos.top = h / 10; // 10% from top pos.bottom = pos.top + this.mOverlaySize; this.mOverlayDrawable.setBounds((int) pos.left, (int) pos.top, (int) pos.right, (int) pos.bottom); } public void onScrollStateChanged(final AbsListView view, final int scrollState) { } public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) { if (totalItemCount - visibleItemCount > 0 && !this.mDragging) { this.mThumbY = ((getHeight() - this.mThumbH) * firstVisibleItem) / (totalItemCount - visibleItemCount); if (this.mChangedBounds) { final int viewWidth = getWidth(); this.mCurrentThumb.setBounds(viewWidth - this.mThumbW, 0, viewWidth, this.mThumbH); this.mChangedBounds = false; } } this.mScrollCompleted = true; if (firstVisibleItem == this.mVisibleItem) { return; } this.mVisibleItem = firstVisibleItem; if (!this.mThumbVisible || this.mScrollFade.mStarted) { this.mThumbVisible = true; this.mCurrentThumb.setAlpha(ScrollFade.ALPHA_MAX); } this.mHandler.removeCallbacks(this.mScrollFade); this.mScrollFade.mStarted = false; if (!this.mDragging) { this.mHandler.postDelayed(this.mScrollFade, 1500); } } private void getSections() { Adapter adapter = this.mList.getAdapter(); if (adapter instanceof HeaderViewListAdapter) { this.mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount(); adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter(); } if (adapter instanceof SectionIndexer) { this.mListAdapter = (BaseAdapter) adapter; this.mSections = ((SectionIndexer) this.mListAdapter).getSections(); } } public void onChildViewAdded(final View parent, final View child) { if (child instanceof ListView) { this.mList = (ListView)child; this.mList.setOnScrollListener(this); getSections(); } } public void onChildViewRemoved(final View parent, final View child) { if (child == this.mList) { this.mList = null; this.mListAdapter = null; this.mSections = null; } } @Override public boolean onInterceptTouchEvent(final MotionEvent ev) { if (this.mThumbVisible && ev.getAction() == MotionEvent.ACTION_DOWN) { if (ev.getX() > getWidth() - this.mThumbW && ev.getY() >= this.mThumbY && ev.getY() <= this.mThumbY + this.mThumbH) { this.mDragging = true; return true; } } return false; } private void scrollTo(final float position) { final int count = this.mList.getCount(); this.mScrollCompleted = false; final Object[] sections = this.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) this.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). final float fPrev = (float) prevSection / nSections; final float fNext = (float) nextSection / nSections; index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev) / (fNext - fPrev)); // Don't overflow if (index > count - 1) { index = count - 1; } this.mList.setSelectionFromTop(index + this.mListOffset, 0); } else { final int index = (int) (position * count); this.mList.setSelectionFromTop(index + this.mListOffset, 0); sectionIndex = -1; } if (sectionIndex >= 0) { final String text = this.mSectionText = sections[sectionIndex].toString(); this.mDrawOverlay = (text.length() != 1 || text.charAt(0) != ' ') && sectionIndex < sections.length; } else { this.mDrawOverlay = false; } } private void cancelFling() { // Cancel the list fling final MotionEvent cancelFling = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0); this.mList.onTouchEvent(cancelFling); cancelFling.recycle(); } @Override public boolean onTouchEvent(final MotionEvent me) { if (me.getAction() == MotionEvent.ACTION_DOWN) { if (me.getX() > getWidth() - this.mThumbW && me.getY() >= this.mThumbY && me.getY() <= this.mThumbY + this.mThumbH) { this.mDragging = true; if (this.mListAdapter == null && this.mList != null) { getSections(); } cancelFling(); return true; } } else if (me.getAction() == MotionEvent.ACTION_UP) { if (this.mDragging) { this.mDragging = false; final Handler handler = this.mHandler; handler.removeCallbacks(this.mScrollFade); handler.postDelayed(this.mScrollFade, 1000); return true; } } else if (me.getAction() == MotionEvent.ACTION_MOVE) { if (this.mDragging) { final int viewHeight = getHeight(); this.mThumbY = (int) me.getY() - this.mThumbH + 10; if (this.mThumbY < 0) { this.mThumbY = 0; } else if (this.mThumbY + this.mThumbH > viewHeight) { this.mThumbY = viewHeight - this.mThumbH; } // If the previous scrollTo is still pending if (this.mScrollCompleted) { scrollTo((float) this.mThumbY / (viewHeight - this.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() { this.mFadeDuration = FADE_DURATION; this.mStartTime = SystemClock.uptimeMillis(); this.mStarted = true; } int getAlpha() { if (!this.mStarted) { return ALPHA_MAX; } int alpha; final long now = SystemClock.uptimeMillis(); if (now > this.mStartTime + this.mFadeDuration) { alpha = 0; } else { alpha = (int) (ALPHA_MAX - ((now - this.mStartTime) * ALPHA_MAX) / this.mFadeDuration); } return alpha; } public void run() { if (!this.mStarted) { startFade(); invalidate(); } if (getAlpha() > 0) { final int y = FastScrollView.this.mThumbY; final int viewWidth = getWidth(); invalidate(viewWidth - FastScrollView.this.mThumbW, y, viewWidth, y + FastScrollView.this.mThumbH); } else { this.mStarted = false; removeThumb(); } } } }