使用 RecyclerView 实现通讯录效果
先附上效果图:
如图效果所示,实现某个字母下面展示以该字母开头的产品。
看界面效果,基本可以肯定,主 View 应该是 RecyclerView 或者 ListView,需要显示出两种布局,字母的布局,产品的布局,我是使用 RecyclerView 实现的,简单记录一下我的思路。
其中一种看起来可以实现的方法是在 Adapter 中设置两个 ViewType,根据不同的 ViewType 加载不同的布局,这条路看起来是走的通的,而且我们很多开发场景加载多套布局都是在 Adapter 中设置和判断 ViewType 实现的。不过我没有这样做,我参考了网上的思路(稍后附上网络参考链接),觉得他们的方式也很不错,走了第二条路。
第二种实现思路是给 RecyclerView 添加了自定义的 RecyclerView.ItemDecoration,这个类很常见的一个用途是自定义 RecyclerView 的分割线,这个实现方式也是,将字母的布局那里当做了分割线来处理,ok,我们来看实现代码。
public class MoreItemDecoration extends RecyclerView.ItemDecoration{
private Context context;
private Paint backgroundPaint;// 画背景的笔
private Paint textPaint;// 画字母的笔
private int titleHeight;
private int textHeight;// 文字高度
private int textBaseLineOffset;// 文字底部位置
private int textStartMargin;// 文字的起始 margin
private ArrayList<IndexMenuChildrenBean> list;// 数据源。 bean 有自己的字母属性
private String currentParentName = "";// 当前的 item 的产品的字母是什么
public MoreItemDecoration(Context context, ArrayList<IndexMenuChildrenBean> list) {
this.context = context;
this.list = list;
this.titleHeight = context.getResources().getDimensionPixelSize(R.dimen.item_decoration_title_height);
backgroundPaint = new Paint();
backgroundPaint.setColor(context.getResources().getColor(R.color.gray));
textPaint = new TextPaint();
textPaint.setColor(context.getResources().getColor(R.color.black));
textPaint.setTextSize(context.getResources().getDimensionPixelSize(R.dimen.item_decoration_title_fontsize));
Paint.FontMetrics fm = textPaint.getFontMetrics();
textHeight = (int) (fm.bottom - fm.top);
textBaseLineOffset = (int) fm.bottom;
textStartMargin = context.getResources().getDimensionPixelOffset(R.dimen.item_decoration_title_start_margin);
}
// 先执行完全部的 getItemOffsets 方法,再去执行 onDraw 方法
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
// 该方法的目的是找到需要在该 position 的 item 的上方空间写字母的 position,然后修改偏移量
// 获取 recyclerView 的 item 的 position
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewAdapterPosition();
if (position == 0) {
outRect.set(0, titleHeight, 0, 0);
} else {
if (!list.get(position - 1).getParentName().equals(list.get(position).getParentName())) {
outRect.set(0, titleHeight, 0, 0);
} else {
outRect.set(0, 0, 0, 0);
}
}
// 判断当前字母是否和 bean 的字母相同 下面方法在数据较多复用 item 时存在 bug
/*if (!currentParentName.equals(list.get(position).getParentName())) {
// 不相同,则绘制分割线空隙为设定的高度
outRect.set(0, titleHeight, 0, 0);
// 将 bean 的字母值赋予给当前的字母值变量
currentParentName = list.get(position).getParentName();
} else {
outRect.set(0, 0, 0, 0);
}*/
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
// 该方法的目的是在需要写字母的 item 的上方写上字母
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int position = params.getViewAdapterPosition();
if (position == 0) {
drawText(c, left, right, child, params, position);
} else {
if (!list.get(position - 1).getParentName().equals(list.get(position).getParentName())) {
// 绘制字母
drawText(c, left, right, child, params, position);
}
}
// 如果当前的字母值和 bean 的字母值相同,则跳过,不再绘制,比如已经绘制了 A 字母,第二个 bean 的字母属性还是 A,则跳过,不再绘制 A。 下面方法在数据较多时存在问题
/*if (currentParentName.equals(list.get(position).getParentName())) {
continue;
}
// 绘制字母
drawText(c, left, right, child, params, position);*/
}
}
private void drawText(Canvas c, int left, int right, View child, RecyclerView.LayoutParams params, int position) {
int rectBottom = child.getTop() - params.topMargin;
c.drawRect(left, rectBottom - titleHeight, right, rectBottom, backgroundPaint);
c.drawText(list.get(position).getParentName(), child.getPaddingLeft() + textStartMargin, rectBottom - (titleHeight - textHeight) / 2 - textBaseLineOffset, textPaint);
// 之前写到,执行完全部的 getItemOffsets 方法后,才执行 onDraw 方法,所以,这里需要再绘制完字母后,修改当前字母变量
//currentParentName = list.get(position).getParentName();
}
}
在 Activity 中调用就十分简单了。
rvMore = (RecyclerView) findViewById(R.id.rv_moreActivity);
rvMore.setLayoutManager(new LinearLayoutManager(MoreActivity.this));
adapter = new MoreAdapter(MoreActivity.this);
rvMore.setAdapter(adapter);
ArrayList<IndexMenuChildrenBean> beans = getBeans();
rvMore.addItemDecoration(new MoreItemDecoration(MoreActivity.this, beans));
adapter.setList(beans);
adapter.notifyDataSetChanged();
参考文章链接:https://juejin.im/entry/586f0640128fe100580a9fd4
作者写的很好,他完整的实现了通讯录的一个功能,并且附有 github 地址,大家可以参考学习,不过他的判断 position 的方式,我一直没有看懂,不过这并不重要,根据自己的实际业务选择合适的方式去判断 position 就可以了。