blob: 54c4b361350c04236bef5509e902ba3286283fe3 [file] [log] [blame]
/*
* 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 android.widget;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
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.TypedValue;
import android.view.MotionEvent;
import android.widget.AbsListView.OnScrollListener;
/**
* Helper class for AbsListView to draw and control the Fast Scroll thumb
*/
class FastScroller {
// Minimum number of pages to justify showing a fast scroll thumb
private static int MIN_PAGES = 4;
// Scroll thumb not showing
private static final int STATE_NONE = 0;
// Not implemented yet - fade-in transition
private static final int STATE_ENTER = 1;
// Scroll thumb visible and moving along with the scrollbar
private static final int STATE_VISIBLE = 2;
// Scroll thumb being dragged by user
private static final int STATE_DRAGGING = 3;
// Scroll thumb fading out due to inactivity timeout
private static final int STATE_EXIT = 4;
private Drawable mThumbDrawable;
private Drawable mOverlayDrawable;
private int mThumbH;
private int mThumbW;
private int mThumbY;
private RectF mOverlayPos;
private int mOverlaySize;
private AbsListView mList;
private boolean mScrollCompleted;
private int mVisibleItem;
private Paint mPaint;
private int mListOffset;
private int mItemCount = -1;
private boolean mLongList;
private Object [] mSections;
private String mSectionText;
private boolean mDrawOverlay;
private ScrollFade mScrollFade;
private int mState;
private Handler mHandler = new Handler();
private BaseAdapter mListAdapter;
private SectionIndexer mSectionIndexer;
private boolean mChangedBounds;
public FastScroller(Context context, AbsListView listView) {
mList = listView;
init(context);
}
public void setState(int state) {
switch (state) {
case STATE_NONE:
mHandler.removeCallbacks(mScrollFade);
mList.invalidate();
break;
case STATE_VISIBLE:
if (mState != STATE_VISIBLE) { // Optimization
resetThumbPos();
}
// Fall through
case STATE_DRAGGING:
mHandler.removeCallbacks(mScrollFade);
break;
case STATE_EXIT:
int viewWidth = mList.getWidth();
mList.invalidate(viewWidth - mThumbW, mThumbY, viewWidth, mThumbY + mThumbH);
break;
}
mState = state;
}
public int getState() {
return mState;
}
private void resetThumbPos() {
final int viewWidth = mList.getWidth();
// Bounds are always top right. Y coordinate get's translated during draw
mThumbDrawable.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH);
mThumbDrawable.setAlpha(ScrollFade.ALPHA_MAX);
}
private void useThumbDrawable(Context context, Drawable drawable) {
mThumbDrawable = drawable;
mThumbW = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.fastscroll_thumb_width);
mThumbH = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.fastscroll_thumb_height);
mChangedBounds = true;
}
private void init(Context context) {
// Get both the scrollbar states drawables
final Resources res = context.getResources();
useThumbDrawable(context, res.getDrawable(
com.android.internal.R.drawable.scrollbar_handle_accelerated_anim2));
mOverlayDrawable = res.getDrawable(
com.android.internal.R.drawable.menu_submenu_background);
mScrollCompleted = true;
getSectionsFromIndexer();
mOverlaySize = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.fastscroll_overlay_size);
mOverlayPos = new RectF();
mScrollFade = new ScrollFade();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(mOverlaySize / 2);
TypedArray ta = context.getTheme().obtainStyledAttributes(new int[] {
android.R.attr.textColorPrimary });
ColorStateList textColor = ta.getColorStateList(ta.getIndex(0));
int textColorNormal = textColor.getDefaultColor();
mPaint.setColor(textColorNormal);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mState = STATE_NONE;
}
void stop() {
setState(STATE_NONE);
}
boolean isVisible() {
return !(mState == STATE_NONE);
}
public void draw(Canvas canvas) {
if (mState == STATE_NONE) {
// No need to draw anything
return;
}
final int y = mThumbY;
final int viewWidth = mList.getWidth();
final FastScroller.ScrollFade scrollFade = mScrollFade;
int alpha = -1;
if (mState == STATE_EXIT) {
alpha = scrollFade.getAlpha();
if (alpha < ScrollFade.ALPHA_MAX / 2) {
mThumbDrawable.setAlpha(alpha * 2);
}
int left = viewWidth - (mThumbW * alpha) / ScrollFade.ALPHA_MAX;
mThumbDrawable.setBounds(left, 0, viewWidth, mThumbH);
mChangedBounds = true;
}
canvas.translate(0, y);
mThumbDrawable.draw(canvas);
canvas.translate(0, -y);
// If user is dragging the scroll bar, draw the alphabet overlay
if (mState == STATE_DRAGGING && 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 (mState == STATE_EXIT) {
if (alpha == 0) { // Done with exit
setState(STATE_NONE);
} else {
mList.invalidate(viewWidth - mThumbW, y, viewWidth, y + mThumbH);
}
}
}
void onSizeChanged(int w, int h, int oldw, int oldh) {
if (mThumbDrawable != null) {
mThumbDrawable.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;
if (mOverlayDrawable != null) {
mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
(int) pos.right, (int) pos.bottom);
}
}
void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
// Are there enough pages to require fast scroll? Recompute only if total count changes
if (mItemCount != totalItemCount && visibleItemCount > 0) {
mItemCount = totalItemCount;
mLongList = mItemCount / visibleItemCount >= MIN_PAGES;
}
if (!mLongList) {
if (mState != STATE_NONE) {
setState(STATE_NONE);
}
return;
}
if (totalItemCount - visibleItemCount > 0 && mState != STATE_DRAGGING ) {
mThumbY = ((mList.getHeight() - mThumbH) * firstVisibleItem)
/ (totalItemCount - visibleItemCount);
if (mChangedBounds) {
resetThumbPos();
mChangedBounds = false;
}
}
mScrollCompleted = true;
if (firstVisibleItem == mVisibleItem) {
return;
}
mVisibleItem = firstVisibleItem;
if (mState != STATE_DRAGGING) {
setState(STATE_VISIBLE);
mHandler.postDelayed(mScrollFade, 1500);
}
}
SectionIndexer getSectionIndexer() {
return mSectionIndexer;
}
Object[] getSections() {
if (mListAdapter == null && mList != null) {
getSectionsFromIndexer();
}
return mSections;
}
private void getSectionsFromIndexer() {
Adapter adapter = mList.getAdapter();
mSectionIndexer = null;
if (adapter instanceof HeaderViewListAdapter) {
mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
}
if (adapter instanceof ExpandableListConnector) {
ExpandableListAdapter expAdapter = ((ExpandableListConnector)adapter).getAdapter();
if (expAdapter instanceof SectionIndexer) {
mSectionIndexer = (SectionIndexer) expAdapter;
mListAdapter = (BaseAdapter) adapter;
mSections = mSectionIndexer.getSections();
}
} else {
if (adapter instanceof SectionIndexer) {
mListAdapter = (BaseAdapter) adapter;
mSectionIndexer = (SectionIndexer) adapter;
mSections = mSectionIndexer.getSections();
} else {
mListAdapter = (BaseAdapter) adapter;
mSections = new String[] { " " };
}
}
}
private void scrollTo(float position) {
int count = mList.getCount();
mScrollCompleted = false;
float fThreshold = (1.0f / count) / 8;
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;
}
int exactSection = section;
sectionIndex = section;
int index = mSectionIndexer.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 = mSectionIndexer.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 = mSectionIndexer.getPositionForSection(section);
if (prevIndex != index) {
prevSection = section;
sectionIndex = section;
break;
} else if (section == 0) {
// When section reaches 0 here, sectionIndex must follow it.
// Assuming mSectionIndexer.getPositionForSection(0) == 0.
sectionIndex = 0;
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 &&
mSectionIndexer.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;
if (prevSection == exactSection && position - fPrev < fThreshold) {
index = prevIndex;
} else {
index = prevIndex + (int) ((nextIndex - prevIndex) * (position - fPrev)
/ (fNext - fPrev));
}
// Don't overflow
if (index > count - 1) index = count - 1;
if (mList instanceof ExpandableListView) {
ExpandableListView expList = (ExpandableListView) mList;
expList.setSelectionFromTop(expList.getFlatListPosition(
ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
} else if (mList instanceof ListView) {
((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
} else {
mList.setSelection(index + mListOffset);
}
} else {
int index = (int) (position * count);
if (mList instanceof ExpandableListView) {
ExpandableListView expList = (ExpandableListView) mList;
expList.setSelectionFromTop(expList.getFlatListPosition(
ExpandableListView.getPackedPositionForGroup(index + mListOffset)), 0);
} else if (mList instanceof ListView) {
((ListView)mList).setSelectionFromTop(index + mListOffset, 0);
} else {
mList.setSelection(index + mListOffset);
}
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();
}
boolean onInterceptTouchEvent(MotionEvent ev) {
if (mState > STATE_NONE && ev.getAction() == MotionEvent.ACTION_DOWN) {
if (isPointInside(ev.getX(), ev.getY())) {
setState(STATE_DRAGGING);
return true;
}
}
return false;
}
boolean onTouchEvent(MotionEvent me) {
if (mState == STATE_NONE) {
return false;
}
final int action = me.getAction();
if (action == MotionEvent.ACTION_DOWN) {
if (isPointInside(me.getX(), me.getY())) {
setState(STATE_DRAGGING);
if (mListAdapter == null && mList != null) {
getSectionsFromIndexer();
}
if (mList != null) {
mList.requestDisallowInterceptTouchEvent(true);
mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
cancelFling();
return true;
}
} else if (action == MotionEvent.ACTION_UP) { // don't add ACTION_CANCEL here
if (mState == STATE_DRAGGING) {
if (mList != null) {
// ViewGroup does the right thing already, but there might
// be other classes that don't properly reset on touch-up,
// so do this explicitly just in case.
mList.requestDisallowInterceptTouchEvent(false);
mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
setState(STATE_VISIBLE);
final Handler handler = mHandler;
handler.removeCallbacks(mScrollFade);
handler.postDelayed(mScrollFade, 1000);
return true;
}
} else if (action == MotionEvent.ACTION_MOVE) {
if (mState == STATE_DRAGGING) {
final int viewHeight = mList.getHeight();
// Jitter
int newThumbY = (int) me.getY() - mThumbH + 10;
if (newThumbY < 0) {
newThumbY = 0;
} else if (newThumbY + mThumbH > viewHeight) {
newThumbY = viewHeight - mThumbH;
}
if (Math.abs(mThumbY - newThumbY) < 2) {
return true;
}
mThumbY = newThumbY;
// If the previous scrollTo is still pending
if (mScrollCompleted) {
scrollTo((float) mThumbY / (viewHeight - mThumbH));
}
return true;
}
}
return false;
}
boolean isPointInside(float x, float y) {
return x > mList.getWidth() - mThumbW && y >= mThumbY && y <= mThumbY + mThumbH;
}
public class ScrollFade implements Runnable {
long mStartTime;
long mFadeDuration;
static final int ALPHA_MAX = 208;
static final long FADE_DURATION = 200;
void startFade() {
mFadeDuration = FADE_DURATION;
mStartTime = SystemClock.uptimeMillis();
setState(STATE_EXIT);
}
int getAlpha() {
if (getState() != STATE_EXIT) {
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 (getState() != STATE_EXIT) {
startFade();
return;
}
if (getAlpha() > 0) {
mList.invalidate();
} else {
setState(STATE_NONE);
}
}
}
}