您的位置:首页 > 移动开发 > 微信开发

RecyclerView+index索引实现仿微信通讯录

2017-01-17 10:05 609 查看
感觉之前写的有点乱,所以有重新整理了一下这个博客:

demo下载地址:http://download.csdn.net/detail/qq_34501274/9799175

最近跟朋友聊天,说道博客相关的事,朋友说,有什么好写的没什么用,你想用的网上都有,又不是什么新东西! 反正我个人不是这样认为的,即使同样的功能,从不会,到上网查资料,看别人的博客,最后自己搞明白了。我认为写博客就跟做笔记一样的。以后在用的着,可以直接在自己博客上找了!这样岂不更好

写之前卡了几片博客:

http://blog.csdn.net/zxt0601/article/details/52420706

http://blog.csdn.net/zxt0601/article/details/52355199

http://blog.piasy.com/2016/03/26/Insight-Android-RecyclerView-ItemDecoration/?utm_source=tuicool&utm_medium=referral

效果图:



1.获取手机通讯录联系人作为真实数据

public static final String[] PHONES_PROJECTION = new String[]
{
Phone.DISPLAY_NAME, //联系人姓名
Phone.NUMBER,       //电话号码

};

/**
* 获取手机联系人
*/
public static List getContactInformation(Context context) {
Uri contact_uri = Phone.CONTENT_URI; //获得联系人默认uri
ContentResolver resolver = context.getContentResolver();  //获得ContentResolver对象
Cursor cursor = resolver.query(contact_uri, PHONES_PROJECTION, null, null, null); //获取电话本中开始一项光标
List contacts = new ArrayList<Contacts>();
if (null != cursor) {
while (cursor.moveToNext()) {
Contacts peson = new Contacts();
/**
* 获取电话号码
*/
String number = cursor.getString(1);
/**
*  当手机号码为空的或者为空字段 跳过当前循环
*/
if (TextUtils.isEmpty(number))
continue;

/**
*  得到联系人名称
*/
String name = cursor.getString(0);

peson.name = name;
peson.phone = number;
/**
*  Pinyin.toPinyin(peson.name.charAt(0))
*  如果是中文转换成字母
* */
String zifuchuang = Pinyin.toPinyin(peson.name.charAt(0));

peson.tag = getSortKey(zifuchuang);
contacts.add(peson);
}
cursor.close();
}
return contacts;
}


通过查询系统数据后,拿到数据后这里只取了名字name和手机号number两个字段,头像等字段什么的没有取。然后通过Pinyin.toPinyin将中文转换成拼音,然后利用getSortKey截取第一个字母并转换成大写字母,存到Bean里,tag就是存这些大写字母的字段。这里中文转换成拼音:

Pinyin的引用
compile 'com.github.promeg:tinypinyin:1.0.0'


getSortKey截取第一个字母并转换成大写字母

/**
* 截取首字母 转换成大写
*/
public static final String getSortKey(String sortKeyString) {
String key = sortKeyString.substring(0, 1).toUpperCase();
if (key.matches("[A-Z]")) {
return key;
}
return "#";
}


因为我们需要把数据按照A-Z的顺序进行排序,这里我们存数据的Bean。

public class Contacts  implements Comparable<Contacts>


然后重写他的方法:

public int compareTo(Contacts o) {
return this.tag.compareTo(o.tag);//这里tag就是用来存储那些大写字母的,所以按这个字段进行排序
}


Bean完成这些后,重载Collections.sort();方法即可,这样就完成了数据字母A—Z的排序

/**
* bean实现Comparable接口  然后重载 Collections.sort(mDatas);方法排序
* A-Z
* **/
Collections.sort(mDatas);


Recyclerview+index的实现

完成上面步骤后,就可以进入我们进入今天要说的Recyclerview+index实现仿微信通讯录

1.首先ItemDecoration的使用:

mRv.setAdapter(mAdapter = new CityAdapter(this, mDatas));
mRv.addItemDecoration(mDecoration = new TitleItemDecoration(this, mDatas));
//        //如果add两个,那么按照先后顺序,依次渲染。
//        //mRv.addItemDecoration(new TitleItemDecoration2(this,mDatas));
//        //分割线
mRv.addItemDecoration(new DividerItemDecoration(MainActivity.this, DividerItemDecoration.VERTICAL_LIST));


直接用addItemDecoration即可,这是两个同名的重载方法:

4000
addItemDecoration(ItemDecoration decor) 常用,(按照add顺序,依次渲染ItemDecoration)

addItemDecoration(ItemDecoration decor, int index) add一个ItemDecoration,并为它指定顺序

在源码中可以看到:



我们再继续跟踪mItemDecorations,发现我们add的ItemDecoration存储在RecyclerView类中的mItemDecorations集合中。



在RecyclerView.ItemDecoration这个类中有三个方法:

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {

super.getItemOffsets(outRect, view, parent, state);

}


-
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {

super.onDraw(c, parent, state);

}


public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {

super.onDrawOver(c, parent, state);

}


如果我们自定义ItemDecoration继承RecyclerView.ItemDecoration,必须重写这三个方法:

如果对ItemDecoration想更深的理解的话可以去看下这篇博客:http://blog.piasy.com/2016/03/26/Insight-Android-RecyclerView-ItemDecoration/?utm_source=tuicool&utm_medium=referral

getItemOffsets方法:

通过 parent 获取 postion 信息,在通过 postion 拿到数据里的每个bean里的分类——tag,因为数据集已经排好序,如果tag与前一个tag不一样,说明是一个新的分类,

则需要绘制头部outRect.set(0, mTitleHeight, 0, 0),否则不需要 outRect.set(0, 0, 0, 0)。

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
if (position > -1) {
if (position == 0) {
outRect.set(0, mTitleHeight, 0, 0);
} else {
/**如果数据  对应position的tag值不为空   且  对应position的tag值与下个数据源position的tag值不一样  需要设置title
*/
if (null != mDatas.get(position).tag && !mDatas.get(position).tag.equals(mDatas.get(position - 1).tag)) {
outRect.set(0, mTitleHeight, 0, 0);//不为空 且跟前一个tag不一样了,说明是新的分类,也要title
} else {
outRect.set(0, 0, 0, 0);
}
}
}
}


这里mTitleHeight 是这样获取的:TypedValue.applyDimension方法是转变为标准尺寸的一个函数

mTitleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, context.getResources().getDisplayMetrics());
titleFontSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, context.getResources().getDisplayMetrics());


onDraw方法:

//绘制在最下层
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
//求出绘制title的位置 大小
//绘制title的开始位置 左边距
int paddingLeft = parent.getPaddingLeft();
//        绘制title的结束位置  右边距
int right = parent.getWidth() - parent.getPaddingRight();
//得到一共需要绘制多少个 title
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
int position = params.getViewLayoutPosition();
//RecyclerView的item position在重置时可能为-1.保险点判断一下吧
if (position > -1) {
if (position == 0) {//等于0肯定要有title的
//绘制title
drawTitleArea(c, paddingLeft, right, child, params, position);

} else {//其他的通过判断
if (null != mDatas.get(position).tag && !mDatas.get(position).tag.equals(mDatas.get(position - 1).tag)) {
//不为空 且跟前一个tag不一样了,说明是新的分类,也要title
drawTitleArea(c, paddingLeft, right, child, params, position);
} else {
//none
}
}
}
}
}

private void drawTitleArea(Canvas c, int left, int right, View child, RecyclerView.LayoutParams params, int position) {//最先调用,绘制在最下层
//设置tilte的背景颜色
mPaint.setColor(COLOR_TITLE_BG);

c.drawRect(left, child.getTop() - params.topMargin - mTitleHeight, right, child.getTop() - params.topMargin, mPaint);
mPaint.setColor(COLOR_TITLE_FONT);
//Paint.getTextBounds(String text, int start, int end, Rect bounds)
mPaint.getTextBounds(mDatas.get(position).tag, 0, mDatas.get(position).tag.length(), mBounds);
c.drawText(mDatas.get(position).tag, child.getPaddingLeft(), child.getTop() - params.topMargin - (mTitleHeight / 2 - mBounds.height() / 2), mPaint);
}


完成上面两步就已经把title绘制好了。

onDrawOver方法:带悬停头部的分组列表

/**绘制在最上层
*/
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int pos = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
String tag = mDatas.get(pos).tag;
//View child = parent.getChildAt(pos);
View child = parent.findViewHolderForLayoutPosition(pos).itemView;//出现一个奇怪的bug,有时候child为空,所以将 child = parent.getChildAt(i)。-》 parent.findViewHolderForLayoutPosition(pos).itemView
mPaint.setColor(COLOR_TITLE_BG);
/**
左   上  右  下   画笔  绘制的区域
*/
c.drawRect(parent.getPaddingLeft(), parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + mTitleHeight, mPaint);
mPaint.setColor(COLOR_TITLE_FONT);
//Paint.getTextBounds(String text, int start, int end, Rect bounds)
mPaint.getTextBounds(tag, 0, tag.length(), mBounds);
c.drawText(tag, child.getPaddingLeft(),
parent.getPaddingTop() + mTitleHeight - (mTitleHeight / 2 - mBounds.height() / 2),
mPaint);
}


下面就开始绘制右侧的索引indexbar了

//    onFinishInflate 当View中所有的子控件均被映射成xml后触发
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);

Log.e("onSizeChanged:-----","走了onSizeChanged"+"---"+w+"---"+h+"---"+oldw+"---"+oldh);
//解决源数据为空 或者size为0的情况,
if (null == mIndexDatas || mIndexDatas.isEmpty()) {
return;
}
mWidth = w;
height = h;
/**
* 得到每个字母的高度
*用右侧indexbar总高度-padingtop和padingbottom=所有字母所占的高度,  然后除以所有字母得到每个字母的高度
* */
mGapHeight = (height - getPaddingTop() - getPaddingBottom()) / mIndexDatas.size();
}

//绘制
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e("onDraw:-----","onDraw");
int t = getPaddingTop();//top的基准点(支持padding)
Rect indexBounds = new Rect();//存放每个绘制的index的Rect区域
String index;//每个要绘制的index内容

for (int i = 0; i < mIndexDatas.size(); i++) {
//得到每个字母
index = mIndexDatas.get(i);
/**getTextBounds
* 四个参数:
* 1.要绘制的内容
* 2.开始位置
* 3.结束未知
* 4.存放每个绘制的index的Rect区域
* */
paint.getTextBounds(index, 0, index.length(), indexBounds);

Paint.FontMetrics fontMetrics = paint.getFontMetrics();//获得画笔的FontMetrics,用来计算baseLine。因为drawText的y坐标,代表的是绘制的文字的baseLine的位置
int baseline = (int) ((mGapHeight - fontMetrics.bottom - fontMetrics.top) / 2);//计算出在每格index区域,竖直居中的baseLine值
canvas.drawText(index, mWidth / 2 - paint.measureText(index) / 2, t + mGapHeight * i + baseline, paint);//调用drawText,居中显示绘制index
}
}


完成这些已经实现了右侧索引indexBar

下面我们给自己的操作添加一些行为响应:

重写onTouchEvent事件主要功能有

a.处理手指按下时的View背景变色,抬起时恢复原来颜色

b.根据手指触摸的落点坐标,判断当前处于哪个index区域

c.回调给相应的监听器处理(显示当前index的值,滑动RecyclerView至相应区域等。。)

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setBackgroundColor(mPressedBackground);//手指按下时背景变色
//注意这里没有break,因为down时,也要计算落点 回调监听器
case MotionEvent.ACTION_MOVE:
float y = event.getY();
//通过计算判断落点在哪个区域:
int p
cae8
ressI = (int) ((y - getPaddingTop()) / mGapHeight);
//边界处理(在手指move时,有可能已经移出边界,防止越界)
if (pressI < 0) {
pressI = 0;
} else if (pressI >= mIndexDatas.size()) {
pressI = mIndexDatas.size() - 1;
}
//回调监听器
if (null != mOnIndexPressedListener) {
mOnIndexPressedListener.onIndexPressed(pressI, mIndexDatas.get(pressI));
}
Log.e("点击区域位置:",pressI+"-----------------------");
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
default:
setBackgroundResource(android.R.color.transparent);//手指抬起时背景恢复透明
//回调监听器
if (null != mOnIndexPressedListener) {
mOnIndexPressedListener.onMotionEventEnd();
}
break;
}
return true;
}
/**
* 当前被按下的index的监听器
*/
private onIndexPressedListener mOnIndexPressedListener;
public interface onIndexPressedListener {
void onIndexPressed(int index, String text);//当某个Index被按下

void onMotionEventEnd();//当触摸事件结束(UP CANCEL)
}
public void setmOnIndexPressedListener(onIndexPressedListener mOnIndexPressedListener) {
this.mOnIndexPressedListener = mOnIndexPressedListener;
}


最后就是实现recyclerview和indexbar的联动

**
* 当按下index的监听
* */
@Override
public void onIndexPressed(int index, String text) {
if (tv_index != null) { //显示TexView
tv_index.setVisibility(View.VISIBLE);
tv_index.setText(text);
}
//        滑动Rv
if (mManager != null) {
int position = getPosByTag(text);
if (position != -1) {
mManager.scrollToPositionWithOffset(position, 0);
}
}
}
/**
* 触摸事件结束
* */
@Override
public void onMotionEventEnd() {
//隐藏TextView
if (tv_index != null) {
tv_index.setVisibility(View.GONE);
}
}
/**
* 根据传入的pos返回tag
*
* @param tag
* @return
*/
private int getPosByTag(String tag) {
if (TextUtils.isEmpty(tag)) {
return -1;
}
/**
* 根据传入的index    遍历集合数据 拿到list<Contacts>中对应的tag   把tag对应的position返回
* */
for (int i = 0; i < mDatas.size(); i++) {
if (tag.equals(mDatas.get(i).tag)) {
return i;
}
}
return -1;
}


这样就完成了RecyclerView与indexbar的联动

整个功能就完成了!

个人表述能力有限,有什么不好的地方欢迎指出
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息