很久没有写博客了,对自己又放松了很多。这篇博客本来早就要写了,迟迟拖到现在。今天这里要说的是,联系人列表,分组带索引,即联系人按字母顺序排列并分组,右边还有索引条。先看下效果:
就是这么一个效果,想必大家都想知道是怎么实现的吧。其实很简单,接下来我会慢慢讲解这个效果的实现。
首先我们分析这个联系人列表的组成:
1.联系人列表 ----- ListView
2.右边索引-------自定义View
3.浮动窗口------WindowManager
4.获取名称的首字母----PinYin4j(已经将github上的封装成了依赖包)
下面我将按照我的实现顺序进行说明:
1.先自定义右边索引。
2.设置浮动窗口。
3.添加列表数据,对数据进行排序,对列表进行分组。
4.添加列表与索引间的监听。
一、自定义索引
这里的索引,我把它命名为IndexView,IndexView 包含26个英文字母和“#”,每个字母所占据的区域位置,IndexView按下时显示黑色背景,每个字母都有文字颜色,触摸时浮动窗口显示不同的字母。这里,我们就有概念了,于是可以开始写代码了:
1.先定义基本的成员变量
/*字母表数组*/
private String letter = "ABCDEFGHIJKLMNOPQRSTUVWXYZ#";
private Paint mPaint;
private int mTextColor;
private int mTextSize;
private int mPadding;
private int mLetterWidth;
private int mLetterHeight;
//当被触摸时,绘制背景
private boolean isTouched;
private int mBackgroundColor;
private int mTransparentColor;
//除了用于绘制外,更涉及到触摸事件
private ArrayList<Rect> mRects;
private OnCharTouchEvent mListener;
//上一次获取的字母
private String mPreLetter;
2.然后开始写逻辑,首先,我们必须继承View,然后重写构造方法,在构造方法中都调用一个init()方法:
public class IndexView extends View
public IndexView(Context context) {
super(context);
init(context);
}
public IndexView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public IndexView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
init里面完成了什么工作呢?主要是对一些成员变量的初始化,其中画笔用来绘制文字的:
private void init(Context context) {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mTextColor = Color.parseColor("#666666");
mBackgroundColor = Color.parseColor("#bbbbbb");
mTransparentColor = Color.parseColor("#00000000");
mRects = new ArrayList<>();
}
3.设置每个字母的位置,每个字母的位置都是一个矩形区域,我们用Rect表示,Rects里面保存所有的字母位置,因此,重写onSizeChanged方法,onSizeChanged方法,是在measure之后调用的,因为在onMeasure里面,我们会得到每个字母的宽和高,
因此这里能够得到每个字母的矩形位置:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRects.clear();
for (int i = 0; i < letter.length(); i++) {
int x = 0;
int y = mLetterHeight * i;
Rect rect = new Rect(x, y, x + mLetterWidth, y + mLetterHeight);
mRects.add(rect);
}
}
4.重写 onMeasure方法,调用于onSizeChanged之前,这里面我们要完成测量当前自定义View的宽和高,以及每个字母的宽和高,这里我们设置字母所占的宽和高是一样的,因此,当前自定义view的宽也就是字母的宽,然后按比例设置了字母的大小,间距等。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//widthMeasureSpec是父类中,调用该类的measure方法设置的
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
mLetterWidth = mLetterHeight = heightSize / letter.length();
mPadding = mLetterHeight / 5;
mTextSize = mLetterHeight - mPadding * 2;
setMeasuredDimension(mLetterWidth, heightSize);
}
5.重写onDraw()方法,主要完成字母的绘制工作。先通过一个变量,判断当前是否是触摸状态,由此设置绘制背景;接着循环绘制每一个字母。
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(isTouched ? mBackgroundColor : mTransparentColor);
for (int i = 0; i < letter.length(); i++) {
char c = letter.charAt(i);
String s = String.valueOf(c);
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);
drawTextCenter(canvas, mPaint, s, mRects.get(i));
}
}
/**
* 将文本绘制在居中位置
*
* @param canvas
* @param paint
* @param s
* @param rect
*/
private void drawTextCenter(Canvas canvas, Paint paint, String s, Rect rect) {
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
int baseline = rect.top + (rect.bottom - rect.top - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(s, rect.centerX(), baseline, paint);
}
drawTextCenter使用之前,需要设置画笔居中,这个方法是根据文本的绘制原理,来将文本绘制在居中位置的。不懂的可以自行百度,谷歌。
到这里,我们已经绘制完一个索引了。但是这是不够的,我们还需要为它添加触摸事件。
6.重写onTouchEvent方法。
/**
* 接收到事件,进行处理,并消费掉
*
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
doDown(event);
break;
case MotionEvent.ACTION_MOVE:
doMove(event);
break;
case MotionEvent.ACTION_UP:
doCancelOrUp();
break;
case MotionEvent.ACTION_CANCEL:
doCancelOrUp();
break;
}
return true;
}
直接返回了true,即所有的触摸事件到这里,都将被消费掉。然后针对不同的事件进行处理。
ActionDown 事件:
private void doDown(MotionEvent event) {
isTouched = true;
int downX = (int) event.getX();
int downY = (int) event.getY();
String s = getTouchedLetter(downX, downY);
Log.v("@s", s + "");
if (mListener != null)
mListener.onTouch(s);
invalidate();
}
记录位置,根据位置获取对应的字母,然后设置监听事件。(这里,先理解,getTouchedLetter为通过坐标位置获取字母,mListener为监听事件,用于回调浮动窗口的)。
ActionMove事件:
/**
* 移动时,判断选中的字母是否发生了变化,并进行回调
*
* @param event
*/
private void doMove(MotionEvent event) {
isTouched = true;
int downX = (int) event.getX();
int downY = (int) event.getY();
String s = getTouchedLetter(downX, downY);
if (s == null)
return;
if (mListener != null) {
if (mPreLetter == null || (!mPreLetter.equalsIgnoreCase(s))) {
mListener.onLetterChanged(mPreLetter, s);
mPreLetter = s;
}
}
invalidate();
}
这里每次都要记录上一个的字母,如果字母发生变化,才回调。
ActionUp|ActionCancel事件:
isTouched = false;
if (mListener != null)
mListener.onRelease();
invalidate();
触摸标志位设置为false,并回调关闭浮动窗口
通过位置获取字母,循环判断坐标位置,包含在哪个字母的Rect矩形位置中,通过这个位置获取字母。
private String getTouchedLetter(int x, int y) {
for (int i = 0; i < mRects.size(); i++) {
Rect rect = mRects.get(i);
if (rect.contains(x, y)) {
return String.valueOf(letter.charAt(i));
}
}
return null;
}
最后一个是监听接口:
public interface OnCharTouchEvent {
void onTouch(String s);
void onLetterChanged(String preLetter, String letter);
void onRelease();
}
到这里,我们的效果是这样的:
今天先到这里,源码提前放出