package com.nolanlawson.supersaiyan.widget;
import android.content.Context;
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.graphics.drawable.StateListDrawable;
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.AbsListView.OnScrollListener;
import android.widget.Adapter;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.HeaderViewListAdapter;
import android.widget.ListView;
import android.widget.SectionIndexer;
import com.nolanlawson.supersaiyan.OverlaySizeScheme;
import com.nolanlawson.supersaiyan.R;
import com.nolanlawson.supersaiyan.util.StringUtil;
import com.nolanlawson.supersaiyan.util.UtilLogger;
/**
* Fast Scroll View that allows for arbitrary sizing of the overlay. Based on
* http://nolanlawson.com/2012/03/19/spruce-up-your-listview-by-dividing-it-into-sections/
*
* @author nolan
*
*/
public class SuperSaiyanScrollView extends FrameLayout
implements OnScrollListener, OnHierarchyChangeListener {
private static UtilLogger log = new UtilLogger(SuperSaiyanScrollView.class);
// how much transparency to use for the fast scroll thumb
private static final int ALPHA_MAX = 255;
// how long before the fast scroll thumb disappears
private static final long FADE_DURATION = 200;
private static final int THUMB_DRAWABLE = R.drawable.fastscroll_thumb_holo;
private static final int THUMB_DRAWABLE_PRESSED = R.drawable.fastscroll_thumb_pressed_holo;
private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed};
private static final int[] STATE_UNPRESSED = new int[]{};
private Drawable mCurrentThumb;
private Drawable mOverlayDrawable;
private int mThumbH;
private int mThumbW;
private int mThumbY;
private RectF mOverlayPos;
// custom values I defined
private int mOverlayWidth;
private int mOverlayHeight;
private float mOverlayTextSize;
private boolean mDragging;
private ListView mList;
private boolean mScrollCompleted;
private boolean mThumbVisible;
private int mVisibleItem;
private Paint mPaint;
private int mListOffset;
private int mOverlayTextColor;
private int mOverlayDrawableResId;
private Object [] mSections;
private String mSectionText;
private boolean mDrawOverlay;
private ScrollFade mScrollFade;
private Handler mHandler = new Handler();
private BaseAdapter mListAdapter;
private boolean mChangedBounds;
private boolean shouldRedrawThumb;
private int mLastW;
private int mLastH;
private int mLastOldw;
private int mLastOldh;
public SuperSaiyanScrollView(Context context) {
super(context);
init(context, null);
}
public SuperSaiyanScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public SuperSaiyanScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private void useThumbDrawable(Drawable drawable) {
mCurrentThumb = drawable;
mThumbW = mCurrentThumb.getIntrinsicWidth();
mThumbH = mCurrentThumb.getIntrinsicHeight();
mChangedBounds = true;
}
private void init(Context context, AttributeSet attrs) {
// set all attributes from xml
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.SuperSaiyanScrollView);
mOverlayHeight = typedArray.getDimensionPixelSize(
R.styleable.SuperSaiyanScrollView_ssjn_overlayHeight,
context.getResources().getDimensionPixelSize(R.dimen.ssjn__overlay_height));
mOverlayWidth = typedArray.getDimensionPixelSize(
R.styleable.SuperSaiyanScrollView_ssjn_overlayWidth,
context.getResources().getDimensionPixelSize(R.dimen.ssjn__overlay_width_normal));
mOverlayTextSize = typedArray.getDimensionPixelSize(
R.styleable.SuperSaiyanScrollView_ssjn_overlayTextSize,
context.getResources().getDimensionPixelSize(R.dimen.ssjn__overlay_text_size_normal));
mOverlayTextColor = typedArray.getColor(R.styleable.SuperSaiyanScrollView_ssjn_overlayTextColor,
context.getResources().getColor(R.color.ssjn__emphasis));
int overlayTheme = typedArray.getInt(R.styleable.SuperSaiyanScrollView_ssjn_overlayTheme, 1);
if (overlayTheme == 0) {
// dark
mOverlayDrawableResId = R.drawable.popup_full_dark;
} else {
// light
mOverlayDrawableResId = R.drawable.popup_full_bright;
}
int schemeIndex = typedArray.getInt(R.styleable.SuperSaiyanScrollView_ssjn_overlaySizeScheme, -1);
if (schemeIndex != -1) {
// use the built-in size schemes
OverlaySizeScheme scheme = OverlaySizeScheme.values()[schemeIndex];
mOverlayTextSize = getContext().getResources().getDimensionPixelSize(scheme.getTextSize());
mOverlayWidth = getContext().getResources().getDimensionPixelSize(scheme.getWidth());
}
typedArray.recycle();
} else {
// no attrs, so initialize with defaults
mOverlayHeight = context.getResources().getDimensionPixelSize(R.dimen.ssjn__overlay_height);
mOverlayWidth = context.getResources().getDimensionPixelSize(R.dimen.ssjn__overlay_width_normal);
mOverlayTextSize = context.getResources().getDimensionPixelSize(R.dimen.ssjn__overlay_text_size_normal);
mOverlayTextColor = context.getResources().getColor(R.color.ssjn__emphasis);
mOverlayDrawableResId = R.drawable.popup_full_bright;
}
log.d("Initialized with mOverlayHeight: %s, mOverlayWidth: %s, mOverlayTextSize: %s, mOverlayTextColor: %s",
mOverlayHeight, mOverlayWidth, mOverlayTextSize, mOverlayTextColor);
// Get both the scrollbar states drawables
final Resources res = context.getResources();
StateListDrawable thumbDrawable = new StateListDrawable();
//This for pressed true
thumbDrawable.addState(STATE_PRESSED,
res.getDrawable(THUMB_DRAWABLE_PRESSED));
//This for pressed false
thumbDrawable.addState(STATE_UNPRESSED,
res.getDrawable(THUMB_DRAWABLE));
useThumbDrawable(thumbDrawable);
mOverlayDrawable = res.getDrawable(mOverlayDrawableResId);
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(mOverlayTextSize);
mPaint.setColor(mOverlayTextColor);
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 SuperSaiyanScrollView.ScrollFade scrollFade = mScrollFade;
int alpha = -1;
if (scrollFade.mStarted) {
alpha = scrollFade.getAlpha();
if (alpha < ALPHA_MAX / 2) {
mCurrentThumb.setAlpha(alpha * 2);
}
int left = viewWidth - (mThumbW * alpha) / 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) {
mCurrentThumb.setState(STATE_PRESSED);
mOverlayDrawable.draw(canvas);
final Paint paint = mPaint;
float descent = paint.descent();
final RectF rectF = mOverlayPos;
if (mSectionText.indexOf('\n') != -1) { // two lines
float textY = (int) (rectF.bottom + rectF.top) / 2 + descent - (paint.getTextSize() / 2);
for (String substring : StringUtil.split(mSectionText, '\n')) {
canvas.drawText(substring, (int) (rectF.left + rectF.right) / 2, textY, paint);
textY += (descent + paint.getTextSize());
}
} else { // one line
canvas.drawText(mSectionText, (int) (rectF.left + rectF.right) / 2, (int) (rectF.bottom + rectF.top) / 2 + descent, paint);
}
} else if (alpha == 0) {
mCurrentThumb.setState(STATE_UNPRESSED);
scrollFade.mStarted = false;
removeThumb();
} else {
mCurrentThumb.setState(STATE_UNPRESSED);
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);
}
updateOverlaySize(w, h, oldw, oldh);
mLastW = w;
mLastH = h;
mLastOldw = oldw;
mLastOldh = oldh;
}
private void updateOverlaySize(int w, int h, int oldw, int oldh) {
final RectF pos = mOverlayPos;
pos.left = (w - mOverlayWidth) / 2;
pos.right = pos.left + mOverlayWidth;
pos.top = h / 10; // 10% from top
pos.bottom = pos.top + mOverlayHeight;
mOverlayDrawable.setBounds((int) pos.left, (int) pos.top,
(int) pos.right, (int) pos.bottom);
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
// ensure that the thumb gets redrawn, even if the user is only fling/touch scrolling
shouldRedrawThumb = true;
}
}
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
if (shouldRedrawThumb) {
super.invalidate(); // force a redraw of the thumb
shouldRedrawThumb = false;
}
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(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;
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();
}
}
}
/**
* Call this whenever the data set changes, in order to update the overlay drawable.
*/
public void refresh() {
getSections();
mDrawOverlay = false; // avoids temporary content flash of previous sections
}
public void setOverlayWidth(int overlayWidth) {
mOverlayWidth = overlayWidth;
updateOverlaySize(mLastW, mLastH, mLastOldw, mLastOldh);
}
public void setOverlayTextSize(float overlayTextSize) {
mOverlayTextSize = overlayTextSize;
if (mPaint != null) {
mPaint.setTextSize(mOverlayTextSize);
}
updateOverlaySize(mLastW, mLastH, mLastOldw, mLastOldh);
}
/**
* Set a size scheme for the overlay that's shown. This will modify both the width of the overlay window
* and the text size.
* @param scheme
*/
public void setOverlaySizeScheme(OverlaySizeScheme scheme) {
mOverlayTextSize = getContext().getResources().getDimensionPixelSize(scheme.getTextSize());
if (mPaint != null) {
mPaint.setTextSize(mOverlayTextSize);
}
mOverlayWidth = getContext().getResources().getDimensionPixelSize(scheme.getWidth());
updateOverlaySize(mLastW, mLastH, mLastOldw, mLastOldh);
}
}