package com.eleybourn.bookcatalogue.widgets;
/*
* Copyright (C) 2008 The Android Open Source Project
*
* This is a substantially modified version of the Android 2.3 FastScroller.
*
* The original did not work correctly with ExpandableListViews; the thumb would work
* for only a small portion of fully expanded views and exhibited odd behaviour with
* view with a small number of groups but large children.
*
* The underlying approach to scrolling with a summary in the original version was
* also flawed: it translated a thumb position of 50% to mean that the middle summaryGroup
* should be visible. While this may seem sensible, it is contrary to reasonable expectations with
* scrollable lists: a thumb at 50% in any scrollable list should result in the list being at
* the mid-point. With an expandableListView, this needs to take into account the total
* number of items (groups and children), NOT just the summary groups. Doing what the original
* implementaion did is not only counter-intuitive, but also makes the thumb unusable in the case of
* n groups, where one of those n has O(n) children, and is expanded. In this case, the entire set
* of children will move through the screen based on the same finger movement as moving between
* two unexpanded groups. In the more general case it can be characterised as uneven scrolling
* if sections have widely varying sizes.
*
* Finally, the original would fail to correctly place the overlay if setFastScrollEnabled was
* called after the Activity had been fuly drawn: this is because the only place that set the
* overlay position was in the onSizeChanged event.
*
* Combine this with the desire to display more than a single letter in the overlay,
* and a rewrite was more or less essential.
*
* The solution is:
*
* - modify init() to fake an onSizeChanged event
* - modify onSizeChanged() to make the overlay 75% of total width;
* - modify draw() to handle arbitrary text (ellipsize if necessary)
* - modify scrollTo() to just deal with list contents and not try to do any fancy
* calculations about group position.
*
* Because the original was in the android package, it had access to classes that we do not
* have access to, so in some cases we now check if mList is an ExpandableListView rather than
* checking if the adapter is an ExpandableListConnector.
*
* *********************************************************
*
* NOTE: any class implementing a SectionIndexer for this object MUST return flattened
* positions in calls to getPositionForSection(), and will be passed flattened positions in
* calls to getSectionForPosition().
*
* *********************************************************
*
* 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.
*/
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.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.SystemClock;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.BaseAdapter;
import android.widget.ExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.HeaderViewListAdapter;
import android.widget.ListView;
import android.widget.SectionIndexer;
import com.eleybourn.bookcatalogue.R;
/**
* Helper class for AbsListView to draw and control the Fast Scroll thumb
*/
public 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;
// ENHANCE: 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 final int mOverlaySize;
private AbsListView mList;
private boolean mScrollCompleted;
private int mVisibleItem;
private TextPaint mPaint;
private int mListOffset;
private int mItemCount = -1;
private boolean mLongList;
private Object [] mSections;
private String mSectionTextV1;
private String[] mSectionTextV2;
private boolean mDrawOverlay;
private ScrollFade mScrollFade;
private int mState;
private Handler mHandler = new Handler();
private BaseAdapter mListAdapter;
private SectionIndexer mSectionIndexerV1;
private SectionIndexerV2 mSectionIndexerV2;
// This value is in SP taken from the Android sources
private static final int mLargeTextSpSize = 22; //Units=SP
private static int mLargeTextScaledSize = 22; //Units=SP
/**
* Better interface that just gets text for rows as needed rather
* than having to build a huge index at start.
*/
public interface SectionIndexerV2 {
String[] getSectionTextForPosition(int position);
}
private boolean mChangedBounds;
public FastScroller(Context context, AbsListView listView) {
mList = listView;
int overlaySize;
// Determine the overlay size based on 3xLargeTextSize; if
// we get an error, just use a hard-coded guess.
try {
final float scale = context.getResources().getDisplayMetrics().scaledDensity;
mLargeTextScaledSize = (int) (mLargeTextSpSize * scale);
overlaySize = (int) (3 * mLargeTextScaledSize);
} catch (Exception e) {
// Not a critical value; just try to get it close.
mLargeTextScaledSize = mLargeTextSpSize;
overlaySize = (int) (3 * mLargeTextScaledSize);
}
mOverlaySize = overlaySize;
init(context);
}
public void setState(int state) {
//System.out.println("State: " + 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
// For reference, the thumb itself is approximately 50% as wide as the underlying graphic
// so 1/6th of the width means the thumb is approximately 1/12 the width.
mThumbW = (int)(mLargeTextScaledSize * 2.5); // viewWidth / 6 ; //mOverlaySize *3/4 ; //64; //mCurrentThumb.getIntrinsicWidth();
mThumbH = (int)(mLargeTextScaledSize * 2.5); //viewWidth / 6 ; //mOverlaySize *3/4; //52; //mCurrentThumb.getIntrinsicHeight();
mThumbDrawable.setBounds(viewWidth - mThumbW, 0, viewWidth, mThumbH);
mThumbDrawable.setAlpha(ScrollFade.ALPHA_MAX);
}
private void useThumbDrawable(Drawable drawable) {
mThumbDrawable = drawable;
// Can't use the view width yet, because it has probably not been set up
// so we just use the native width. It will be set later when we come to
// actually draw it.
mThumbW = mThumbDrawable.getIntrinsicWidth();
mThumbH = mThumbDrawable.getIntrinsicHeight();
mChangedBounds = true;
}
private void init(Context context) {
// Get both the scrollbar states drawables
final Resources res = context.getResources();
useThumbDrawable(res.getDrawable( R.drawable.scrollbar_handle_accelerated_anim2));
mOverlayDrawable = res.getDrawable( R.drawable.menu_submenu_background);
mScrollCompleted = true;
getSections();
mOverlayPos = new RectF();
mScrollFade = new ScrollFade();
mPaint = new TextPaint();
mPaint.setAntiAlias(true);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(mOverlaySize / 3);
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;
// Send a fake onSizeChanged so that overlay position is correct if
// this is called after Activity is stable
final int w = mList.getWidth();
final int h = mList.getHeight();
onSizeChanged(w,h,w,h);
}
void stop() {
setState(STATE_NONE);
// No need for these any more.
mOverlayDrawable = null;
mThumbDrawable = null;
}
boolean isVisible() {
return !(mState == STATE_NONE);
}
public void draw(final 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) {
final TextPaint paint = mPaint;
float descent = paint.descent();
final RectF rectF = mOverlayPos;
// Work out what we actually need to draw
boolean has2Lines;
String line1;
String line2 = "";
// If there is no V2 data, use the V1 data
if (mSectionTextV2 == null) {
has2Lines = false;
line1 = mSectionTextV1;
} else {
// If using V2 data, make sure line 1 is a valid straing
if (mSectionTextV2[0] == null)
line1 = "";
else
line1 = mSectionTextV2[0];
// Check if line 2 is present
if (mSectionTextV2.length > 1 && mSectionTextV2[1] != null && !mSectionTextV2[1].equals("")) {
has2Lines = true;
line2 = mSectionTextV2[1];
} else {
has2Lines = false;
}
}
// If there are two lines, expand the box
if (has2Lines) {
Rect pos = mOverlayDrawable.getBounds();
Rect posSave = new Rect(pos);
pos.set(pos.left, pos.top, pos.right, pos.bottom + pos.height()/2);
mOverlayDrawable.setBounds(pos);
mOverlayDrawable.draw(canvas);
mOverlayDrawable.setBounds(posSave);
} else {
mOverlayDrawable.draw(canvas);
}
// Draw the first line
final String text1 = TextUtils.ellipsize(line1, paint, (mOverlayPos.right - mOverlayPos.left) * 0.8f, TextUtils.TruncateAt.END).toString();
canvas.drawText(text1, (int) (rectF.left + rectF.right) / 2,
// Base of text at: (middle) + (half text height) - descent : so it is vertically centred
(int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 6 - descent,
paint);
if (has2Lines) {
// Draw the second line, but smaller than first
float s = paint.getTextSize();
paint.setTextSize(s * 0.7f);
final String text2 = TextUtils.ellipsize(line2, paint, (mOverlayPos.right - mOverlayPos.left) * 0.8f, TextUtils.TruncateAt.END).toString();
canvas.drawText(text2, (int) (rectF.left + rectF.right) / 2,
// Base of text at: (middle) + (half text height) - descent : so it is vertically centred
(int) (rectF.bottom + rectF.top) / 2 + mOverlaySize / 6 + s,
paint);
paint.setTextSize(s);
}
} 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;
// Original: width was equal to height, controlled by mOverlaySize.
// pos.left = (w - mOverlaySize) / 2;
// pos.right = pos.left + mOverlaySize;
//
// Now, Make it 75% of total available space
pos.left = (w / 8);
pos.right = pos.left + w * 3 / 4;
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);
}
}
private void getSections() {
Adapter adapter = mList.getAdapter();
mSectionIndexerV1 = null;
mSectionIndexerV2 = null;
if (adapter instanceof HeaderViewListAdapter) {
mListOffset = ((HeaderViewListAdapter)adapter).getHeadersCount();
adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter();
}
if (mList instanceof ExpandableListView) {
ExpandableListAdapter expAdapter = ((ExpandableListView)mList).getExpandableListAdapter();
if (expAdapter instanceof SectionIndexer) {
mSectionIndexerV1 = (SectionIndexer) expAdapter;
mListAdapter = (BaseAdapter) adapter;
mSections = mSectionIndexerV1.getSections();
} else if (expAdapter instanceof SectionIndexerV2) {
mSectionIndexerV2 = (SectionIndexerV2) expAdapter;
mListAdapter = (BaseAdapter) adapter;
}
} else {
if (adapter instanceof SectionIndexer) {
mListAdapter = (BaseAdapter) adapter;
mSectionIndexerV1 = (SectionIndexer) adapter;
mSections = mSectionIndexerV1.getSections();
} else if (adapter instanceof SectionIndexerV2) {
mListAdapter = (BaseAdapter) adapter;
mSectionIndexerV2 = (SectionIndexerV2) adapter;
} else {
mListAdapter = (BaseAdapter) adapter;
mSections = new String[] { " " };
}
}
}
private void scrollTo(float position) {
int count = mList.getCount();
mScrollCompleted = false;
final Object[] sections = mSections;
int sectionIndex;
int index = (int) (position * count);
if (mList instanceof ListView) { // This INCLUDES ExpandableListView
((ListView) mList).setSelectionFromTop(index + mListOffset, 0);
} else {
mList.setSelection(index + mListOffset);
}
if (mSectionIndexerV2 != null) {
mSectionTextV2 = mSectionIndexerV2.getSectionTextForPosition(index);
} else {
if (sections != null && sections.length > 1) {
sectionIndex = mSectionIndexerV1.getSectionForPosition(index);
if (sectionIndex >=0 && sectionIndex < sections.length)
mSectionTextV1 = sections[sectionIndex].toString();
else
mSectionTextV1 = null;
} else {
sectionIndex = -1;
mSectionTextV1 = null;
}
}
if ( (mSectionTextV2 != null ) || (mSectionTextV1 != null && mSectionTextV1.length() > 0)) {
mDrawOverlay = true; //(mSectionText.length() != 1 || mSectionText.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 (ev.getX() > mList.getWidth() - mThumbW && ev.getY() >= mThumbY &&
ev.getY() <= mThumbY + mThumbH) {
setState(STATE_DRAGGING);
return true;
}
}
return false;
}
boolean onTouchEvent(MotionEvent me) {
if (mState == STATE_NONE) {
return false;
}
if (me.getAction() == MotionEvent.ACTION_DOWN) {
if (me.getX() > mList.getWidth() - mThumbW
&& me.getY() >= mThumbY
&& me.getY() <= mThumbY + mThumbH) {
setState(STATE_DRAGGING);
if (mListAdapter == null && mList != null) {
getSections();
}
cancelFling();
return true;
}
} else if (me.getAction() == MotionEvent.ACTION_UP) {
if (mState == STATE_DRAGGING) {
setState(STATE_VISIBLE);
final Handler handler = mHandler;
handler.removeCallbacks(mScrollFade);
handler.postDelayed(mScrollFade, 1000);
return true;
}
} else if (me.getAction() == 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;
}
// ENHANCE would be nice to use ViewConfiguration.get(context).getScaledTouchSlop()???
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;
}
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);
}
}
}
}