转载

android自定义控件之索引控件的实现

IndexView用于为ListView添加索引。

首先看一下两个demo的效果,分别是音乐列表和联系人列表:

android自定义控件之索引控件的实现 android自定义控件之索引控件的实现

如何实现

构造函数

public IndexView(Context context, AttributeSet attrs, int defStyle);
public IndexView(Context context, AttributeSet attrs);
public IndexView(Context context);

构造函数中做了如下几件事。

1 读取自定义属性

自定义属性包括如下这些。

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="IndexView">
<attr name="heightOccupy" format="float" />
<attr name="indexTextColor" format="color" />
<attr name="selectIndexTextColor" format="color" />
<attr name="selectIndexBgColor" format="color" />
<attr name="indexTextSizeScale" format="float" />
<attr name="tipTextColor" format="color" />
<attr name="tipBg" format="color|reference" />
</declare-styleable>
</resources>

描述如下:

属性名 描述
heightOccupy 0.0f-1.0f, 索引占据的高度比例,小于1的部分将分别在上下两端留白
indexTextColor 未选中的索引字体颜色
selectIndexTextColor 选中的索引字体颜色
selectIndexBgColor 选中的索引背景色
indexTextSizeScale 0.0f-1.0f, 控制索引字体相对大小, 默认0.65f
tipTextColor 提示框字体颜色
tipBg 提示框背景

2 初始化提示框

提示框就是用来提示刚刚点击或者滑到的索引的符号。并且提示框可以在显示一段时间后自动隐藏。

3 初始化用于绘制的画笔

onMeasure方法

该方法确定控件所要占据的宽和高。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
if(widthMode != MeasureSpec.EXACTLY){
width = Math.min(DEFAULT_WIDTH, width);
}
singleIndexHeight = height * heightOccupy / INDEXES.length();
indexTextSize = Math.min(singleIndexHeight, width) * indexTextSizeScale;
indexTextPaint.setTextSize(indexTextSize);
selectIndexTextPaint.setTextSize(indexTextSize);
setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}

onDraw方法

该方法实现对控件的绘制。

@Override
protected void onDraw(Canvas canvas) {

super.onDraw(canvas);
float singleIndexWidth = getMeasuredWidth();
for (int i = 0; i < INDEXES.length(); i++) {

float x = (singleIndexWidth - indexTextPaint.measureText(INDEXES, i, i + 1)) / 2;
float baseline = (1 - heightOccupy) / 2.0f * height + i * singleIndexHeight + Util.getBaseline(0, singleIndexHeight, indexTextPaint);

if(i == selectIndex){
float left = 0.1f * singleIndexWidth;
float top = (1 - heightOccupy) / 2.0f * height + i * singleIndexHeight + 0.05f * singleIndexHeight;
float right = 0.9f * singleIndexWidth;
float bottom = (1 - heightOccupy) / 2.0f * height + i * singleIndexHeight + 0.95f * singleIndexHeight;
canvas.drawRoundRect(new RectF(left, top, right, bottom), 5, 5, selectIndexBgPaint);
canvas.drawText(String.valueOf(INDEXES.charAt(i)), x, baseline, selectIndexTextPaint);
}else{
canvas.drawText(String.valueOf(INDEXES.charAt(i)), x, baseline, indexTextPaint);
}
}
}

触摸事件

这里有一个注意点,当触摸到一个新的索引时,必然需要显示一个提示框,但不一定会切换到这个新的索引,因为这个新索引不一定存在数据。

@Override
public boolean onTouchEvent(MotionEvent event) {

float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (tipVisible) {
this.removeCallbacks(hideTipRunnable);
}
break;
case MotionEvent.ACTION_UP:
this.postDelayed(hideTipRunnable, TIP_SHOW_TIME);
break;
}
int newSelectIndex = getSelectByY(y);
if (selectIndex != newSelectIndex && onIndexChangeListener != null) {
onIndexChangeListener.OnIndexChange(newSelectIndex, INDEXES.charAt(newSelectIndex));
tip.setText(String.valueOf(INDEXES.charAt(newSelectIndex)));
if (!tipVisible) {
tipVisible = true;
tip.setVisibility(VISIBLE);
}
}
return true;
}

自定义监听器

IndexView提供了1个接口OnIndexChangeListener,由触摸事件检测到索引变化时触发。本接口不需要使用者自己实现该接口。

protected interface OnIndexChangeListener {
void OnIndexChange(int select, char index);
}

如何绑定IndexView和ListView

IndexView和ListView显然是耦合的。

一方面,当触摸索引时,IndexView需要访问到ListView确认这个新的索引是否存在数据,以此决定是否切换索引,如果存在数据,还要通知ListView切换到相应的位置去;

另一方面,当滑动ListView时,随着ListView的位置变化,需要通知IndexView将索引切换到对应的位置去。

上面所述,通过抽象类Binder类来实现。在bind()方法中通过给IndexView设置一个OnIndexChangeListener监听器,给ListView设置一个OnScrollListener监听器就实现了二者的绑定。

使用本控件的人只需要实例化一个继承Binder的匿名内部类的对象即可,并且只需要实现 public abstract String getListItemKey(int position); 这个abstract函数即可。

getListItemKey()这个方法的意思是为每一个列表项指定一个用来索引的字符串。比如在demo1中用来索引的是歌曲名称,在demo2中用来索引的是用户名,如果在demo1中想改为用歌手名来索引,可以很方便的进行修改。

这里有一个注意点,当由于触摸IndexView导致ListView滑动时,也会触发ListView的OnScrollListener,这时需要避免掉IndexView再次发生索引切换,使用了一个标记flag来解决。

public abstract class Binder {

private ListView listView;
private IndexView indexView;
private boolean flag = false;

public Binder(ListView listView, IndexView indexView){
this.listView = listView;
this.indexView = indexView;
}

public void bind(){
indexView.setOnIndexChangeListener(new IndexView.OnIndexChangeListener() {
@Override
public void OnIndexChange(int selectIndex, char index) {
ListAdapter adapter = listView.getAdapter();
int pos = -1;
for (int i = 0; i < adapter.getCount(); i++) {
char currentIndex = Util.getIndex(getListItemKey(i));
if (currentIndex == index) {
pos = i;
break;
}
}
if (pos != -1) {
listView.setSelection(pos);
flag = true;
indexView.setSelectIndex(selectIndex);
}
}
});

listView.setOnScrollListener(new AbsListView.OnScrollListener() {

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if(flag){
flag = false;
return;
}
indexView.setIndex(Util.getIndex(getListItemKey(firstVisibleItem)));
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
});
}
public abstract String getListItemKey(int position);
}

拼音排序器

在获取了ListView的数据之后,需要先对数据进行排序。

使用抽象类PinyinComparator可以很方便的实现。该类已经实现了两个字符串的比较函数,使用时只要对实体类(java bean)进行排序即可。

public abstract class PinyinComparator<T> implements Comparator<T> {
public abstract int compare(T s1, T s2);

public int compare(String s1, String s2) {
char i1 = Util.getIndex(s1);
char i2 = Util.getIndex(s2);
if(i1 == '#' && i2 == '#'){
return Util.getStringForSort(s1).compareTo(Util.getStringForSort(s2));
}else if(i1 == '#'){
return 1;
}else if(i2 == '#'){
return -1;
}else{
return Util.getStringForSort(s1).compareTo(Util.getStringForSort(s2));
}
}
}

demo1中的使用的例子如下,仅需要使用匿名内部类,实现抽象方法即可。

Collections.sort(items, new PinyinComparator<Item>() {
@Override
public int compare(Item s1, Item s2) {
return compare(s1.getSong(), s2.getSong());
}
});

汉字转拼音使用了开源项目jpinyin。

原文  http://zhikaizhang.cn/2016/08/20/android自定义控件之索引控件的实现/
正文到此结束
Loading...