本文介绍了Android ItemDecoration 实现分组索引列表的示例代码,分享给大家。具体如下:
先来看看效果:
我们要实现的效果主要涉及三个部分:
- 分组 GroupHeader
- 分割线
- SideBar
前两个部分涉及到一个ItemDecoration类,也是我们接下来的重点,该类是RecyclerView的一个抽象静态内部类,主要作用就是给RecyclerView的ItemView绘制额外的装饰效果,例如给RecyclerView添加分割线。
使用ItemDecoration时需要继承该类,根据需求可以重写如下三个方法,其它的方法已经deprecated了:
代码语言:javascript复制public class GroupHeaderItemDecoration extends RecyclerView.ItemDecoration {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
}
然后将其添加到RecyclerView中:
代码语言:javascript复制recyclerView.addItemDecoration(new GroupHeaderItemDecoration())
了解这个三个方法的作用,这样才能更好的实现我们想要的功能:
1、getItemOffsets()
给指定的ItemView设置偏移量,具体怎么设置呢,咱们看图说话:
图中左边的是原始RecyclerView列表,右边是设置了ItemView偏移量的列表,其实相当于在ItemView外部添加了一个矩形区域 其中left、top、right、bottom就是ItemView在四个方向的偏移量,对应的设置代码如下:
代码语言:javascript复制outRect.set(left, top, right, bottom)
在我们的分组索引列表中,只需要对ItemView设置顶部的偏移量,其它三个偏移量为0即可。这样就可以在ItemView顶部预留出一定高度的区域,如下图:
2、onDraw()
在getItemOffsets()方法中,我们设置了偏移量,进而得到了对应的偏移区域,接下来在onDraw()中就可以给ItemView绘制装饰效果了,所以我们在该方法中将分组索引列表中的GroupHeader的内容绘制在ItemView顶部偏移区域里。也就是绘制前边 gif 图里的A、B、C… GroupHeader,虽然看起来像一个个独立的ItemView,但并不是的哦!
注意该绘制操作会在ItemView的onDraw()前完成的!
3、onDrawOver()
该方法同样也是用来绘制的,但是它在ItemDecoration的onDraw()方法和ItemView的onDraw()完成后才执行。所以其绘制的内容会遮挡在RecyclerView上,因此我们可以在该方法中绘制分组索引列表中悬浮的GroupHeader,也就是在列表顶部随着列表滚动切换的GroupHeader。
一、分组GroupHeader
三个方法的作用已经解释完了,接下来就是代码实现我们的效果了:
首先保证RecyclerView的数据源已经按照某种规律进行了分组排序,具体什么规律你说了算,我们例子中按照数据源中指定字段的值的首字母升序排列,也就是常见通讯录的排序方式。然后在每个data中保存需要在GroupHeader上显示的内容,可以使用tag字段,我们这里保存的是对应的首字母。这里没必要将整个数据源设置到ItemDecoration里边,所以我们只需要提取排序后数据源的tag保存到列表中,然后设置到ItemDecoration里边,后边的操作就依赖设置的数据源了,根据tag的异同来决定是否绘制GroupHeader等。
上边已经分析了,GroupHeader只在列表中每组数据对应的第一个ItemView顶部显示,只需要对ItemView设置顶部的偏移量即可:
代码语言:javascript复制public class GroupHeaderItemDecoration extends RecyclerView.ItemDecoration {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
RecyclerView.LayoutManager manager = parent.getLayoutManager();
//只处理线性垂直类型的列表
if ((manager instanceof LinearLayoutManager)
&& LinearLayoutManager.VERTICAL != ((LinearLayoutManager) manager).getOrientation()) {
return;
}
int position = parent.getChildAdapterPosition(view);
//ItemView的position==0 或者 当前ItemView的data的tag和上一个ItemView的不相等,则为当前ItemView设置top 偏移量
if (!Utils.listIsEmpty(tags) && (position == 0 || !tags.get(position).equals(tags.get(position - 1)))) {
outRect.set(0, groupHeaderHeight, 0, 0);
}
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
}
其中tags就是我们设置到ItemDecoration的数据源,是一个String集合。groupHeaderHeight就是ItemView的顶部偏移量。
之后就是在ItemView的顶部偏移区域绘制GroupHeader了:
代码语言:javascript复制public class GroupHeaderItemDecoration extends RecyclerView.ItemDecoration {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
for (int i = 0; i < parent.getChildCount(); i ) {
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);
String tag = tags.get(position);
//和getItemOffsets()里的条件判断类似,开始绘制分组的GroupHeader
if (!Utils.listIsEmpty(tags) && (position == 0 || !tag.equals(tags.get(position - 1)))) {
drawGroupHeader(c, parent, view, tag);
}
}
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
private void drawGroupHeader(Canvas c, RecyclerView parent, View view, String tag) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int bottom = view.getTop() - params.topMargin;
int top = bottom - groupHeaderHeight;
c.drawRect(left, top, right, bottom, mPaint);
int x = left groupHeaderLeftPadding;
int y = top (groupHeaderHeight Utils.getTextHeight(mTextPaint, tag)) / 2;
c.drawText(tag, x, y, mTextPaint);
}
}
绘制GroupHeader就是Canvasc操作,先绘制一个矩形框,再绘制相应的文字,当然绘制图片也是没问题的,其中groupHeaderLeftPadding是个可配置字段,代表绘制的文字或图片到列表左边沿的距离,也可以理解为GroupHeader的左padding。
最后就是悬浮在顶部的GroupHeader绘制了:
代码语言:javascript复制public class GroupHeaderItemDecoration extends RecyclerView.ItemDecoration {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if (!show) {
return;
}
//列表第一个可见的ItemView位置
int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
String tag = tags.get(position);
View view = parent.findViewHolderForAdapterPosition(position).itemView;
//当前ItemView的data的tag和下一个itemView的不相等,则代表将要重新绘制悬停的GroupHeader
boolean flag = false;
if (!Utils.listIsEmpty(tags) && (position 1) < tags.size() && !tag.equals(tags.get(position 1))) {
//如果第一个可见ItemView的底部坐标小于groupHeaderHeight,则执行Canvas向上位移操作
if (view.getBottom() <= groupHeaderHeight) {
c.save();
flag = true;
c.translate(0, view.getHeight() view.getTop() - groupHeaderHeight);
}
}
drawSuspensionGroupHeader(c, parent, tag);
if (flag) {
c.restore();
}
}
private void drawSuspensionGroupHeader(Canvas c, RecyclerView parent, String tag) {
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
int bottom = groupHeaderHeight;
int top = 0;
c.drawRect(left, top, right, bottom, mPaint);
int x = left groupHeaderLeftPadding;
int y = top (groupHeaderHeight Utils.getTextHeight(mTextPaint, tag)) / 2;
c.drawText(tag, x, y, mTextPaint);
}
}
绘制操作和onDraw中的类似,gif 中有一个悬浮GroupHeader上移的动画,就是通过Canvas位移来实现的,注意在Canvas位移的前后进行save()和restore()操作。
我们给GroupHeaderItemDecoration提供了设置GroupHeader左padding、高度、背景色、文字颜色、尺寸、以及是否显示顶部悬浮GroupHeader的方法,方便使用。
关于绘制操作需要注意的是,GroupHeader所在的偏移区域和ItemView是相互独立的,不要把GroupHeader当做ItemView的一部分哦。到这里GroupHeader的功能就实现了,只需要将GroupHeaderItemDecoration添加到RecyclerView即可。
至于如何通过layout或者View来实现GroupHeader,做过一些尝试,效果都不理想,期待大家的好想法哦!
这里先用一个接口,对外提供自定义绘制GroupHeader的方法:
代码语言:javascript复制public interface OnDrawItemDecorationListener {
/**
* 绘制GroupHeader
* @param c
* @param paint 绘制GroupHeader区域的paint
* @param textPaint 绘制文字的paint
* @param params 共四个值left、top、right、bottom 代表GroupHeader所在区域的四个坐标值
* @param position 原始数据源中的position
*/
void onDrawGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position);
/**
* 绘制悬浮在列表顶部的GroupHeader
*/
void onDrawSuspensionGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position);
}
二、分割线
现在RecyclerView还差一个分割线,当前最笨的办法可以在ItemView的布局文件中设置,既然系统都提供了ItemDecoration,那用它来优雅的实现为何不可呢,我们只需要给列表中每组数据除了最后一项数据对应的ItemView之外的添加分割线即可,也就是不给每组数据对应的最后一个ItemView添加分割线。很简单,直接上核心代码:
代码语言:javascript复制public class DivideItemDecoration extends RecyclerView.ItemDecoration {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
RecyclerView.LayoutManager manager = parent.getLayoutManager();
//只处理线性垂直类型的列表
if ((manager instanceof LinearLayoutManager)
&& LinearLayoutManager.VERTICAL != ((LinearLayoutManager) manager).getOrientation()) {
return;
}
int position = parent.getChildAdapterPosition(view);
if (!Utils.listIsEmpty(tags) && (position 1) < tags.size() && tags.get(position).equals(tags.get(position 1))) {
//当前ItemView的data的tag和下一个ItemView的不相等,则为当前ItemView设置bottom 偏移量
outRect.set(0, 0, 0, divideHeight);
}
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
for (int i = 0; i < parent.getChildCount(); i ) {
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);
//和getItemOffsets()里的条件判断类似
if (!Utils.listIsEmpty(tags) && (position 1) < tags.size() && tags.get(position).equals(tags.get(position 1))) {
drawDivide(c, parent, view);
}
}
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
}
private void drawDivide(Canvas c, RecyclerView parent, View view) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
int left = parent.getPaddingLeft();
int right = parent.getWidth();
int top = view.getBottom() params.bottomMargin;
int bottom = top divideHeight;
c.drawRect(left, top, right, bottom, mPaint);
}
}
三、SideBar
SideBar就是 gif 图右边的垂直字符条,是一个自定义View。手指触摸选中一个字符,则列表会滚动到对应的分组头部位置。实现起来也蛮简单的,核心代码如下:
代码语言:javascript复制public class SideBar extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//重新计算SideBar宽高
if (heightMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.AT_MOST) {
getMaxTextSize();
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = (maxHeight 15) * indexArray.length;
}
if (widthMode == MeasureSpec.AT_MOST) {
widthSize = maxWidth 10;
}
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onDraw(Canvas canvas) {
for (int i = 0; i < indexArray.length; i ) {
String index = indexArray[i];
float x = (mWidth - mTextPaint.measureText(index)) / 2;
float y = mMarginTop mHeight * i (mHeight Utils.getTextHeight(mTextPaint, index)) / 2;
//绘制字符
canvas.drawText(index, x, y, mTextPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
// 选中字符的下标
int pos = (int) ((event.getY() - mMarginTop) / mHeight);
if (pos = 0 && pos < indexArray.length) {
setBackgroundColor(TOUCH_COLOR);
if (onSideBarTouchListener != null) {
for (int i = 0; i < tags.size(); i ) {
if (indexArray[pos].equals(tags.get(i))) {
onSideBarTouchListener.onTouch(indexArray[pos], i);
break;
} else {
onSideBarTouchListener.onTouch(indexArray[pos], -1);
}
}
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setBackgroundColor(UNTOUCH_COLOR);
if (onSideBarTouchListener != null) {
onSideBarTouchListener.onTouchEnd();
}
break;
}
return true;
}
}
在onMeasure()方法里,如果SideBar的宽、高测量模式为MeasureSpec.AT_MOST则重新计算SideBar的宽、高。onDraw()方法则是遍历索引数组,并绘制字符索引。在onTouchEvent()方法里,我们根据手指在SideBar上触摸坐标点的y值,计算出触摸的相应字符,以便在OnSideBarTouchListener接口进行后续操作,例如列表的跟随滚动等等。
四、实例
前边已经完成了三大核心功能,最后来愉快的使用下吧:
代码语言:javascript复制public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list);
SideBar sideBar = (SideBar) findViewById(R.id.side_bar);
final TextView tip = (TextView) findViewById(R.id.tip);
final List<ItemData datas = new ArrayList< ();
ItemData data = new ItemData("北京");
datas.add(data);
ItemData data1 = new ItemData("上海");
datas.add(data1);
ItemData data2 = new ItemData("广州");
datas.add(data2);
.
.
.
ItemData data34 = new ItemData("Hello China");
datas.add(data34);
ItemData data35 = new ItemData("宁波");
datas.add(data35);
SortHelper<ItemData sortHelper = new SortHelper<ItemData () {
@Override
public String sortField(ItemData data) {
return data.getTitle();
}
};
sortHelper.sortByLetter(datas);//将数据源按指定字段首字母排序
List<String tags = sortHelper.getTags(datas);//提取已排序数据源的tag值
MyAdapter adapter = new MyAdapter(this, datas, false);
final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
//添加分割线
recyclerView.addItemDecoration(new DivideItemDecoration().setTags(tags));
//添加GroupHeader
recyclerView.addItemDecoration(new GroupHeaderItemDecoration(this)
.setTags(tags)//设置tag集合
.setGroupHeaderHeight(30)//设置GroupHeader高度
.setGroupHeaderLeftPadding(20));//设置GroupHeader 左padding
recyclerView.setAdapter(adapter);
sideBar.setOnSideBarTouchListener(tags, new OnSideBarTouchListener() {
@Override
public void onTouch(String text, int position) {
tip.setVisibility(View.VISIBLE);
tip.setText(text);
if ("↑".equals(text)) {
layoutManager.scrollToPositionWithOffset(0, 0);
return;
}
//滚动列表到指定位置
if (position != -1) {
layoutManager.scrollToPositionWithOffset(position, 0);
}
}
@Override
public void onTouchEnd() {
tip.setVisibility(View.GONE);
}
});
}
}
这也就是文章开头的 gif 效果。如果需要自定义ItemView的绘制可以这样写:
代码语言:javascript复制recyclerView.addItemDecoration(new GroupHeaderItemDecoration(this)
.setTags(tags)
.setGroupHeaderHeight(30)
.setGroupHeaderLeftPadding(20)
.setOnDrawItemDecorationListener(new OnDrawItemDecorationListener() {
@Override
public void onDrawGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position) {
c.drawRect(params[0], params[1], params[2], params[3], paint);
int x = params[0] Utils.dip2px(context, 20);
int y = params[1] (Utils.dip2px(context, 30) Utils.getTextHeight(textPaint, tags.get(position))) / 2;
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher, null);
Bitmap icon1 = Bitmap.createScaledBitmap(icon, Utils.dip2px(context, 20), Utils.dip2px(context, 20), true);
c.drawBitmap(icon1, x, params[1] Utils.dip2px(context, 5), paint);
c.drawText(tags.get(position), x Utils.dip2px(context, 25), y, textPaint);
}
@Override
public void onDrawSuspensionGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position) {
c.drawRect(params[0], params[1], params[2], params[3], paint);
int x = params[0] Utils.dip2px(context, 20);
int y = params[1] (Utils.dip2px(context, 30) Utils.getTextHeight(textPaint, tags.get(position))) / 2;
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher, null);
Bitmap icon1 = Bitmap.createScaledBitmap(icon, Utils.dip2px(context, 20), Utils.dip2px(context, 20), true);
c.drawBitmap(icon1, x, params[1] Utils.dip2px(context, 5), paint);
c.drawText(tags.get(position), x Utils.dip2px(context, 25), y, textPaint);
}
})
);
坐标计算有点复杂了……0_o……
看下效果:
当然不止于此,更多的效果等待着机智的你去创造。
更多代码细节及用法可参考:https://github.com/Othershe/GroupIndexLib
以上就是本文的全部内容,希望对大家的学习有所帮助。