// 索引条核心类
public class IndexScroller {
private static final int STATE_HIDDEN = 0;
private static final int STATE_SHOWING = 1;
private static final int STATE_SHOWN = 2;
private static final int STATE_HIDING = 3;
private float mIndexbarWidth; // 索引条宽度,高度肯定是显示所有字符的, 所以高度是看内容的
private float mIndexbarMargin; // 索引条距离屏幕右侧边缘的距离 , 索引条中的文字和边框的padding 也取这个值
private float mPreviewPadding; // 预览框中的文字距离边框的长度
private float mDensity; // 屏幕当前密度/标准密度(160)
private float mScaledDensity; // 用在字体上的, 和上面一样, 当前密度/160
private float mAlphaRate; // 透明度, 用于显示和隐藏索引条(包括文本) 0-1
private int mState = STATE_HIDDEN; // 索引条的状态
private int mListViewWidth; // listview 的宽
private int mListViewHeight; // listview的高
private int mCurrentSection = -1; // 当前被选中的索引项位置
private boolean mIsIndexing = false;
private ListView mListView = null; // 指向封装在内部的listview
private SectionIndexer mIndexer = null; // 外部接口, 提供给调用时自定义一些方法来实现功能
private String[] mSections = null; // 索引列表中的所有文本
private RectF mIndexbarRect; // 索引条区域
/***
* 所有变量的初始化, 有三个地方, 1 构造函数中直接的定义索引条的宽和padding等 2
* setAdapter(mListView.getAdapter());
* 通过adapter把SectionIndexer对象和mSections传递进来, 3 onSizeChanged , 当父控件ListView
* onSizeChanged 被调用的时候, 调用本类中的onSizeChanged 将listview的高和宽传递进来
*
*/
/***
* 构造方法初始化变量
*/
public IndexScroller(Context ctx, ListView lv) {
// 通过上下文获取当前屏幕密度比值
mDensity = ctx.getResources().getDisplayMetrics().density;
// 比值,字体对应的密度比值
mScaledDensity = ctx.getResources().getDisplayMetrics().scaledDensity;
// 因为IndexScroller 只是作为listView的核心部分, 具体的变量和方法, 其实是在ListView中定义的
// 所以必须传递Listview进来进行初始化和绑定
mListView = lv;
// 绑定SectionIndexer接口的实现方法
setAdapter(mListView.getAdapter());
// 在标准屏幕下(160px) , 该控件宽20, 乘以密度比值得到实际需要的宽度px
mIndexbarWidth = 20 * mDensity; // 实际大小, 索引条宽度
mIndexbarMargin = 10 * mDensity; // 实际大小, 索引条距离屏幕右侧边缘的距离 ,
mPreviewPadding = 5 * mDensity; // 实际大小, 预览框中的文字距离边框的长度
}
/**
*
* 调用类中创建的Adapter绑定ListView , 且该 adapter中实现SectionIndexer接口方法
* 于是在ListView.setAdapter方法被调用的时候, 调用核心类scroller类中此处的setAdapter
* 使用adapter这种方式就能很好的实现方法和变量的传递,得到需要调用的SectionIndexer对象中的方法和mSections
*
* @param adapter
*/
public void setAdapter(Adapter adapter) {
if (adapter instanceof SectionIndexer) {
mIndexer = (SectionIndexer) adapter;
mSections = (String[]) mIndexer.getSections();
}
}
/***
* 保持最新的高和宽 初始化 mIndexbarRect , mListViewWidth ,mListViewHeight
* 这个方法必须保证在draw方法之前调用, 因为draw方法中首先就需要这个mIndexbarRect来绘制索引框
*/
public void onSizeChanged(int w, int h, int oldw, int oldh) {
mListViewWidth = w;
mListViewHeight = h;
mIndexbarRect = new RectF(w - mIndexbarWidth - mIndexbarMargin,
mIndexbarMargin, w - mIndexbarMargin, h - mIndexbarMargin);
}
/***
* 绘制索引条和预览文本
*/
public void draw(Canvas canvas) {
// 1 绘制索引条, 包括索引条背景和文本
// 只在某些状态才绘制
if (mState == STATE_HIDDEN) {
return;
}
// 设置画笔属性准备绘制索引条背景
Paint indexbarPaint = new Paint();
indexbarPaint.setColor(Color.BLACK);
indexbarPaint.setAlpha((int) (64 * mAlphaRate)); // 半透明
// 绘制索引条背景 (第二,第三个参数指定圆角半径)
canvas.drawRoundRect(mIndexbarRect, 5 * mDensity, 5 * mDensity,
indexbarPaint);
// 绘制好索引条背景后, 先绘制预览正方形, 再绘制索引条中的文本
if (mSections != null && mSections.length > 0) {
// 绘制预览框和文本
// 如果显示索引条的同时, 选中了索引条中的某项则显示预览
if (mCurrentSection >= 0) {
Paint previewPaint = new Paint();
previewPaint.setColor(Color.BLACK);
previewPaint.setAlpha(96);
Paint previewTextPaint = new Paint();
previewTextPaint.setColor(Color.WHITE);
previewTextPaint.setTextSize(50 * mScaledDensity); // 按密度比设置字体像素大小
// 测量设置的单个字符的宽度
float previewTextWidth = previewTextPaint
.measureText(mSections[mCurrentSection]);
// 计算预览框高度 = 上下边距 + 文本基线以下长度 - 文本基线以上的长度(负数)
// 文本基线以上是ascent() , 值为负数
// 文本基线以下是descent(), 值为正数
float previewSize = 2 * mPreviewPadding
+ previewTextPaint.descent()
- previewTextPaint.ascent();
// 定义预览背景正方形区域
// 屏幕的宽度也就是listview的宽度 , 预览框是正方形 , 预览框宽度 = previewSize 计算高度
// left = (listview的宽度 - 预览框的宽度) / 2
// top = (屏幕高度 - 预览框的高度) / 2
// right = (listview的宽度 - 预览框的宽度) / 2 + 预览框的宽度
// bottom = (屏幕高度 - 预览框的宽度) / 2 + 预览框的高度
RectF previewRect = new RectF(
(mListViewWidth - previewSize) / 2,
(mListViewHeight - previewSize) / 2,
(mListViewWidth - previewSize) / 2 + previewSize,
(mListViewHeight - previewSize) / 2 + previewSize);
// 绘制背景
canvas.drawRoundRect(previewRect, 5 * mDensity, 5 * mDensity,
previewPaint);
// 绘制文本 , drawText(String text, float x, float y, Paint paint)
// y -- 这个参数是文本基线的y轴坐标 = 边框top + 边距 + 基线以上部分(也就是减去assent 负数)
canvas.drawText(mSections[mCurrentSection], previewRect.left
+ (previewSize - previewTextWidth) / 2, previewRect.top
+ mPreviewPadding - previewTextPaint.ascent(),
previewTextPaint);
}
}
// 绘制索引条里面的文本
Paint indexPaint = new Paint();
indexPaint.setColor(Color.WHITE);
indexPaint.setAlpha((int) (255 * mAlphaRate));
indexPaint.setTextSize(12 * mScaledDensity); // 设置好了字符画笔就能获得字体的宽高
// 每个索引项的可用高度
float sectionHeight = (mIndexbarRect.height() - 2 * mIndexbarMargin)
/ mSections.length;
// paddingTop = (索引项的可用高度 - 文本的高度) /2
float paddingTop = (sectionHeight - (indexPaint.descent() - indexbarPaint
.ascent())) / 2;
for (int i = 0; i < mSections.length; i++) {
// indexPaint.measureText 获取字符的宽 , 然后用索引条的宽减去字符宽再除以2得到两侧边距
float paddingLeft = (mIndexbarWidth - indexPaint
.measureText(mSections[i])) / 2;
canvas.drawText(mSections[i], mIndexbarRect.left + paddingLeft,
mIndexbarRect.top + mIndexbarMargin + paddingTop
+ sectionHeight * i - indexPaint.ascent(),
indexPaint);
}
}
// 实现索引条的显示和状态切换
private Handler mHandler = new Handler() {
@Override
public void handleMessage(android.os.Message msg) {
super.handleMessage(msg);
switch (mState) {
case STATE_SHOWING:
mAlphaRate += (1 - mAlphaRate) * 0.2;
if (mAlphaRate > 0.9) {
mAlphaRate = 1;
setState(STATE_SHOWN);
}
mListView.invalidate(); // 刷新父控件, 导致本控件draw方法不断重绘
fade(10); // 消息处理中, 调用fade方法发送消息, 形成循环, 进行渐变处理
break;
case STATE_SHOWN:
setState(STATE_HIDING);
break;
case STATE_HIDING:
mAlphaRate -= mAlphaRate * 0.2;
if (mAlphaRate < 0.1) {
mAlphaRate = 0;
setState(STATE_HIDDEN);
}
mListView.invalidate();
fade(10);
break;
}
};
};
private void fade(long delay) {
mHandler.removeMessages(0); // 清除未执行完的消息
mHandler.sendEmptyMessageAtTime(0, SystemClock.uptimeMillis() + delay);
}
protected void setState(int state) {
if (state < STATE_HIDDEN || state > STATE_HIDING)
return;
mState = state;
switch (mState) {
case STATE_HIDDEN: // 已经隐藏, 取消所有显示效果
mHandler.removeMessages(0);
break;
case STATE_SHOWING: // 逐渐显示, 通过fade方法向handler发送消息
mAlphaRate = 0;
fade(0);
break;
case STATE_SHOWN: // 长期显示
mHandler.removeMessages(0);
break;
case STATE_HIDING: // 逐渐隐藏
mAlphaRate = 1;
fade(3000);
break;
}
}
/**
* 事件
*/
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: // 手指按下事件
if (mState != STATE_HIDDEN && contains(ev.getX(), ev.getY())) { // 如果当前索引条已经显示,
// 且手指是在索引条范围内
setState(STATE_SHOWN); // 那么设置状态为STATE_SHOWN , 就不会自动隐藏索引条
// mIsIndexing 赋值为真,表示当前控件是索引条 ,
mIsIndexing = true;
// 根据选中的索引项, 让listView选中 (显示)对应的item
mCurrentSection = getSectionByPoint(ev.getY());
mListView.setSelection(mIndexer
.getPositionForSection(mCurrentSection));
return true;
}
break;
case MotionEvent.ACTION_MOVE: // 手指在索引条上按下且滑动
if (mIsIndexing) {
if (contains(ev.getX(), ev.getY())) {
mCurrentSection = getSectionByPoint(ev.getY());
mListView.setSelection(mIndexer
.getPositionForSection(mCurrentSection));
}
return true;
}
break;
case MotionEvent.ACTION_UP:
if (mIsIndexing) {
mIsIndexing = false;
mCurrentSection = -1;
}
if (mState == STATE_SHOWN) {
setState(STATE_HIDING);
}
break;
}
return false;
}
/***
* 根据事件Y坐标, 判断当前选中的文本
*
* @param y
* @return
*/
private int getSectionByPoint(float y) {
// 如果索引项为空则直接返回0
if (mSections == null || mSections.length < 1) {
return 0;
}
// 如果y坐标在索引条顶部范围外则返回0
if (y < mIndexbarRect.top + mIndexbarMargin) {
return 0;
}
// 如果Y 坐标超出索引条底部范围, 则返回最后一个下标
if (y >= mIndexbarRect.top + mIndexbarRect.height() - mIndexbarMargin) {
return mSections.length - 1;
}
// 计算位置得出下标且返回
return (int) ((y - mIndexbarRect.top - mIndexbarMargin) / ((mIndexbarRect
.height() - 2 * mIndexbarMargin) / mSections.length));
}
/**
* 判断手指按下的区域是否是索引条内
*
* @param x
* @param y
* @return
*/
public boolean contains(float x, float y) {
// 这里对x 没有很严格的限制 , 因为索引条本身就太窄了, 而且距离屏幕右边的距离就很小, 所以不用考虑屏幕右边一点间距
return x > mIndexbarRect.left && y > mIndexbarRect.top
&& y < mIndexbarRect.top + mIndexbarRect.height();
}
public void show() {
if (mState == STATE_HIDDEN) {
setState(STATE_SHOWING);
} else if (mState == STATE_HIDING) {
setState(STATE_HIDING);
}
}
public void hide() {
if (mState == STATE_SHOWN) {
setState(STATE_HIDING);
}
}
}
//ListView , IndexScoller是绘制在这个Listview里面的
public class IndexableListView extends ListView {
private boolean mIsFastScrollEnabled = false;
private IndexScroller mIndexScroller = null;
private GestureDetector mGestureDector = null;
public IndexableListView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public IndexableListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public IndexableListView(Context context) {
super(context);
}
@Override
@ExportedProperty
public boolean isFastScrollEnabled() {
return mIsFastScrollEnabled;
}
@Override
public void setFastScrollEnabled(boolean enabled) {
mIsFastScrollEnabled = true;
if (mIsFastScrollEnabled) {
// 设置允许快速滑动 , 初始化mIndexScroller
if (mIndexScroller == null) {
mIndexScroller = new IndexScroller(getContext(), this);
}
} else {
// 设置不允许快速滑动, 则判断是否索引条存在则隐藏且销毁索引条
if (mIndexScroller != null) {
mIndexScroller.hide();
mIndexScroller = null;
}
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas); // 调用父类draw方法绘制listview基础的部分
// 调用scroller的draw方法绘制, 为了安全加个判断
if (mIndexScroller != null) {
mIndexScroller.draw(canvas);
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// 如果事件是在索引条的范围内则 直接调用scroller的onTouchEvent
if (mIndexScroller != null && mIndexScroller.onTouchEvent(ev)) {
return true;
}
// 针对快速滑动事件
if (mGestureDector == null) {
mGestureDector = new GestureDetector(getContext(),
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
if (mIndexScroller != null) {
mIndexScroller.show();
}
return super.onFling(e1, e2, velocityX, velocityY);
}
});
}
mGestureDector.onTouchEvent(ev);
return super.onTouchEvent(ev);
}
/***
* 如果touchEvent是索引条的, 那么就阻断Listview的事件处理 让索引条处理onTouch
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mIndexScroller.contains(ev.getX(), ev.getY())) {
return true;
}
return super.onInterceptTouchEvent(ev);
}
/***
* 作为ListView而言adapter需要完成的工作是列表项的具体显示, 如果是自定义的adapter类的话也就是里面的getView方法 ,
* 也可以使用系统自带的布局文件 但是在这里的adapter不仅需要完成列表项的显示, 更重要的是实现 SectionIndexer 接口方法,
* 提供了从section获取Position 等方法 因此这里在setAdapter 的时候, 需要把adapter传递给scroller核心类 ,
* 然后在scroller中把这个SectionIndexer实现对象取出,
* 于是scroller中就可以通过SectionIndexer读取到所有的section , 为listview设置选中的section 等操作
*
*/
@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
if (mIndexScroller != null) {
mIndexScroller.setAdapter(adapter);
}
}
/***
* onSizeChanged 需要让scroller时时更新该listview的高和宽,且时时更新索引条的矩形区域
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mIndexScroller.onSizeChanged(w, h, oldw, oldh);
}
}
// Activity , 包含了ListView需要的继承自arrayAdapter的内部类,且实现了SectionIndex接口
public class MainActivity extends Activity {
private ArrayList<String> mItems;
IndexableListView mListview;
// private IndexableListView mListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mItems = new ArrayList<String>();
mItems.add("Diary of a Wimpy Kid 6: Cabin Fever");
mItems.add("Steve Jobs");
mItems.add("Inheritance (The Inheritance Cycle)");
mItems.add("11/22/63: A Novel");
mItems.add("The Hunger Games");
mItems.add("The LEGO Ideas Book");
mItems.add("Explosive Eighteen: A Stephanie Plum Novel");
mItems.add("Catching Fire (The Second Book of the Hunger Games)");
mItems.add("Elder Scrolls V: Skyrim: Prima Official Game Guide");
mItems.add("Death Comes to Pemberley");
mItems.add("Diary of a Wimpy Kid 6: Cabin Fever");
mItems.add("Steve Jobs");
mItems.add("Inheritance (The Inheritance Cycle)");
mItems.add("11/22/63: A Novel");
mItems.add("The Hunger Games");
mItems.add("The LEGO Ideas Book");
mItems.add("Explosive Eighteen: A Stephanie Plum Novel");
mItems.add("Catching Fire (The Second Book of the Hunger Games)");
mItems.add("Elder Scrolls V: Skyrim: Prima Official Game Guide");
mItems.add("Death Comes to Pemberley");
mItems.add("Shanghai");
mItems.add("Beijing");
mItems.add("Tianjing");
mItems.add("Guangzhou");
mItems.add("Suzhou");
mItems.add("Nanjing");
mItems.add("Hainan");
mItems.add("Xizang");
Collections.sort(mItems);
mListview = (IndexableListView) findViewById(R.id.id_indexListView);
ContentAdapter adapter = new ContentAdapter(this,
android.R.layout.simple_list_item_1, mItems);
mListview.setAdapter(adapter);
mListview.setFastScrollEnabled(true);
}
class ContentAdapter extends ArrayAdapter<String> implements SectionIndexer {
// 索引关键字字符串
private String mSections = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public ContentAdapter(Context context, int resource,
List<String> objects) {
super(context, resource, objects);
}
/***
* 返回索引项数组 这里每个索引项都只有一个字符, 所以索引项数组的是单个字符串数组
*/
@Override
public Object[] getSections() {
String[] sections = new String[mSections.length()];
for (int i = 0; i < mSections.length(); i++) {
sections[i] = String.valueOf(mSections.charAt(i)); // 每个索引项section都是一个单个的字符串
}
return sections;
}
/***
* 根据给出的索引项, 查找匹配的列表项, 并返回第一个匹配的列表项位置
*/
@Override
public int getPositionForSection(int sectionIndex) {
// i -- 索引项下标, 如果当前索引项没有匹配的列表项, 那么则查找前一个索引项
for (int i = sectionIndex; i >= 0; i--) {
// 遍历ArrayList中所有item , j 是列表项的下标
for (int j = 0; j < getCount(); j++) {
if (i == 0) { // 查找数字开头的
// 从0到9 , 只要找到第一个匹配的就返回该列表项
for (int k = 0; k <= 9; k++) {
if (StringMatcher.match(getItem(j),
String.valueOf(k))) {
return j;
}
}
} else {
// 匹配索引项字母开头的
String key = String.valueOf(mSections.charAt(i));
if (StringMatcher.match(getItem(j), key)) {
return j;
}
}
}
}
return 0;
}
@Override
public int getSectionForPosition(int position) {
return 0;
}
}
}
//StringMatcher 比较字符串前缀是否匹配
public class StringMatcher {
public static boolean match(String value, String key) {
if (value == null || key == null)
return false;
if (value.length() < key.length()) {
return false;
}
int i = 0; // value 位置指针
int j = 0; // key 位置指针
// 思路是如果key是value开头的一部分的话, 那么i, j记录的是比对过的位数 , i == j
do {
// 逐位比对 , key 一定是位数比较短的字符串
if (value.charAt(i) == key.charAt(j)) {
i++;
j++;
} else if (j > 0) {
// 如果value和key 的当前比对字符不相等的话, 但是j>0 , 表示前面比对过前缀, 且前面比对过的的部分是相等的,
// 只是到了第j位字符不同, 于是这时前缀匹配成功, 应该退出比对.
break;
} else {
// 前缀匹配, 中间不匹配会执行到上面j>0后退出循环
// 只有一开始就不匹配才会执行到这个地方 ,不匹配于是 i, j 初始就是0, 是相等的, 不能直接退出,
// 需要给i++ , 表示已经匹配比较过第i位字符 , 因为我们只比较前缀,可以直接退出了.. 如果需要比较中间匹配的话, 那这里就不要break ,
// 最后返回的时候判断 i>j 则表示, value里面包含key
i++;
break;
}
} while (i < value.length() && j < key.length());
return (i == j) ? true : false;
}
}
//布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<com.example.hendry_indexable_listview.IndexableListView
android:id="@+id/id_indexListView"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>