前两篇不管是ListView还是RecyclerView都是根据ViewType来实现的,是将分类标题看成分类的一种来建模实现的,本篇则使用RecyclerView中ItemDecoration来实现分类的标题显示,如果对本篇有点晕的还请先移步到前两篇文章,传送门:Android分类列表之ListView-ViewType实现和Android分类列表之RecyclerView-ViewType实现
一、分析思路
由于一些代码逻辑和布局和前两篇是一致的,所以接下来涉及到这些重复的代码区域将会简单的一笔带过,重点放到我们的核心ItemDecoration中
-
创建
RecyclerView
适配器ItemDecorationRecyclerAdapter
,因为前两篇都是根据ViewType来实现分类列表的,所以会有两种布局当然也就有两种ViewHolder了(CouponViewHolder
和TitleViewHolder
),而使用ItemDecoration
则不需要将分类Title
看成是一种ViewType,所以ItemDecorationRecyclerAdapter
只有一种ViewHolder即:CouponViewHolder
,由于基本逻辑是一致的,就是少了TitleViewHolder
,所以这里不在详细讲诉逻辑了,直接贴出ItemDecorationRecyclerAdapter
代码:public class ItemDecorationRecyclerAdapter extends RecyclerView.Adapter<ItemDecorationRecyclerAdapter.CouponViewHolder> { private List<CouponBean> mCouponBeanList; private Context mContext; public ItemDecorationRecyclerAdapter(List<CouponBean> couponBeanList, Context context) { mCouponBeanList = couponBeanList; mContext = context; } @NonNull @Override public CouponViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { return new CouponViewHolder(LayoutInflater.from(mContext).inflate(R.layout.yunfei_coupon_item2, viewGroup, false)); } @Override public void onBindViewHolder(@NonNull CouponViewHolder couponViewHolder, int i) { couponViewHolder.bindData(mCouponBeanList.get(i)); } @Override public int getItemCount() { if (mCouponBeanList != null) { return mCouponBeanList.size(); } return 0; } static class CouponViewHolder extends RecyclerView.ViewHolder { private TextView couponDescription, couponMoney, indate, useCondition; public CouponViewHolder(@NonNull View itemView) { super(itemView); couponDescription = itemView.findViewById(R.id.couponDescription); couponMoney = itemView.findViewById(R.id.couponMoney); useCondition = itemView.findViewById(R.id.useCondition); indate = itemView.findViewById(R.id.indate); } void bindData(CouponBean couponBean) { couponDescription.setText(couponBean.getCouponUseExplain()); couponMoney.setText(couponBean.getDenominationShow()); useCondition.setText(couponBean.getUseConditionsDesc()); indate.setText(couponBean.getValidityDate()); } } }
-
关于
RecyclerView
的ItemDecoration
,是Google为了更灵活的给RecyclerView
的item
之间添加一些更炫酷可自定义的内容,最简单的就是添加分割线条,在创建完ItemDecoartion
后通过recyclerView.addItemDecoration()
将其添加到recyclerView
中,我们先看一下ItemDecoration
吧。
1)ItemDecoration
是RecyclerView
中的一个静态的抽象类如下:public abstract static class ItemDecoration { public ItemDecoration() { } public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { this.onDraw(c, parent); } /** @deprecated */ @Deprecated public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) { } public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { this.onDrawOver(c, parent); } /** @deprecated */ @Deprecated public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) { } /** @deprecated */ @Deprecated public void getItemOffsets(@NonNull Rect outRect, int itemPosition, @NonNull RecyclerView parent) { outRect.set(0, 0, 0, 0); } public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { this.getItemOffsets(outRect, ((RecyclerView.LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent); } }
其中废弃的方法函数我们就不在关注了,直接看其他三个核型方法,这里先讲一下三个方法的调用顺序
getItemOffsets->onDraw->onDrawOver
,所以我们就按照其调用的顺序来了解这三个方法的作用。
2)第一个public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
,这个方法的主要作用是通过outRect
给item之间预留一定的空间类似margin或者padding值,这样估计太抽象,我们来实践操作一下,假如我们先不设定outRect
,我们来打印出outRect
对应属性值:public class TestItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); Log.e("hailong", "getItemOffsets:" + outRect.left+":"+outRect.top+":"+outRect.right+":"+outRect.bottom); } }
在通过
RecyclerView.addItemDecoration()
将我们定义好的ItemDecoration
添加进去,效果如图:运行效果图 对应代码日志输出 由此观察可以发现在我们未做任何操作时,
outRect
对应的left、top、right、bottom
都为0,而且所有的item 都是挤在一起的。现在我们把对应的属性值都给50
即:public class TestItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); outRect.set(50,50,50,50); Log.e("hailong", "getItemOffsets:" + outRect.left+":"+outRect.top+":"+outRect.right+":"+outRect.bottom); } }
我们再来看一下效果:
运行效果图 对应代码日志输出 对比上面的运行结果,发现Item的上下左右全部都有了一定的边距,至此我们应该隐约感受到
outRect
对item上下左右的边距是有一定的控制作用的且对应边的值就是对应边间距大小,我们来进行简单验证,假如只给outRect的left值
为300的话:public class TestItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); outRect.left = 300; Log.e("hailong", "getItemOffsets:" + outRect.left+":"+outRect.top+":"+outRect.right+":"+outRect.bottom); } }
我们来看一下效果:
运行效果图 对应代码日志输出 所以我们可以得出结论:
outRect
中的left、top、right、bottom
就是itemView对应的左上右下的间距值,如图:运行效果图 在我们的分类显示中,对应的
分类Title
正好也就在对应的Item
之间,所以我们只要在对应的两个分类过度的itemView之间留出足够的空间(top和bottom
),再想办法将分类标题
添加上去就行了。
3)public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
顾名思义,就是绘制,这个方法就是绘制我们想要给RecyclerView
添加的内容,我们正好可以用它来绘制我们的分类标题;它绘制优先高于ItemView,也就是说RecyclerView
先调用ItemDecoration
中的onDraw
方法绘制我们自己定义好的绘制内容,然后才回去绘制我们的ItemView
,如果对应绘制Item内容位置有冲突,ItemView
是会覆盖遮掩住对应ItemDecoration.onDraw
绘制的内容的,所以方法ondraw
要结合方法getItemOffsets
来使用,在预留的空间间隙中进行绘制,才能完成显示要添加的内容,当然如果实际的需求需要遮挡一些的话或者绘制的内容占据更多的空间,我们也可以灵活的随机应变。4)
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
此方法和public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
从参数上来看,没什么太大的区别,都是绘制我们想要为RecyclerView
添加的内容,其区别主要是在调用时机的不同,RecyclerView
会现调用ItemDecoration
中的onDraw
方法进行我们底层内容的绘制,然后是绘制ItemView
,最后是调用ItemDecoration
中的onDrawOver
方法绘制顶层的内容。当然上面我们可以将分类Title
绘制到底层对应在ItemView
预留出来的空间处,实现分类显示;了解这个方法后,我们还可以将分类Title
绘制到最顶层且对应在ItemView
预留出来的空间处;
二、编码和测试
上面简单的分析一下关于
ItemDecoration
的相关api和实现分类列表的方法,下面我们开始编码实现功能,且分别使用底层绘制onDraw
和顶层绘制onDrawOver
来实现
-
创建类
TypeItemDecoration
,因为我们需要绘制分类Title
到View上,所以我们需要定义属性文字的大小TextSize
和文字画笔TextPaint
;分类数据集合List<CouponBean> mCouponBeanList
用来判断是否类型发生的转换即分类Title的位置
;分类标题内容String[] titles = {"当前可用", "不满足条件"}
;根据文字画笔测量title的绘制高度textHeight
和分类Title的所占用
的高度rectHeight
,创建对应的构造方法:public class TypeItemDecoration extends RecyclerView.ItemDecoration { /** * outRect高度 */ private final int rectHeight; /** * 文字画笔 */ private final TextPaint mTextPaint; /** * X轴方向的距离 */ private final int marginX; /** * Adapter数据 */ private List<CouponBean> mCouponBeanList; /** * 标题 */ private final String[] titles = {"当前可用", "不满足条件"}; /** * 文字高度 */ private final int textHeight; public TypeItemDecoration(Context context, List<CouponBean> couponBeanList, int textSize) { super(); this.mCouponBeanList = couponBeanList; this.rectHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0x32, context.getResources().getDisplayMetrics()); this.marginX = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0x0f, context.getResources().getDisplayMetrics()); this.mTextPaint = new TextPaint(); this.mTextPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize, context.getResources().getDisplayMetrics())); this.mTextPaint.setColor(Color.BLACK); this.textHeight = (int) (mTextPaint.getFontMetrics().bottom - mTextPaint.getFontMetrics().top); } }
-
重写方法
getItemOffsets
,为了我们的显示更加的美观,把item之间空白的和item之间有分类Title
的间距进行区分,让占有分类Title
的间距稍微大一些为50dp
,反之则为其一半
,且给最后item和底部一定距离为50dp
,这样也是为了看起会更好看,代码如下:/** * position == mCouponBeanList.size() - 1 ? rectHeight : 0 * 这是为了判断当前item是否是当前类的最后一个数据且是当前集合中最后一个数据,是则给底部留出一个title个高度显得更加美观 * * @param outRect * @param view * @param parent * @param state */ @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { int position = parent.getChildAdapterPosition(view); if (position != -1) { if (position == 0) { outRect.set(marginX, rectHeight, marginX, position == mCouponBeanList.size() - 1 ? rectHeight : 0); } else if (!mCouponBeanList.get(position).getIsUse().equals(mCouponBeanList.get(position - 1).getIsUse())) { outRect.set(marginX, rectHeight, marginX, position == mCouponBeanList.size() - 1 ? rectHeight : 0); } else { outRect.set(marginX, rectHeight >> 1, marginX, position == mCouponBeanList.size() - 1 ? rectHeight : 0); } } }
其中我们需要对
position
进行判断,为什么?点进源码我们可以发现如下:
从源码的角度来看他是有可能为-1
的,所以我们最好最一个判断以防万一,然后就是判断对应的情况来设置间距,第一个判断position==0
肯定分类中第一类的开始,所以要留出绘制分类Title
高度为rectHeight
的空间,第二个判断!mCouponBeanList.get(position).getIsUse().equals(mCouponBeanList.get(position - 1).getIsUse())
是同样判断当前item是否为新分类的第一个item
,如果是则预留出高度为rectHeight
的空间,反之则为rectHeight
的一半,此时我们运行看一下还没绘制分类Title
时的效果:
</tr>
</table>
|
-
绘制
分类标题
,上面分析过,我们可以分别采用底层绘制和顶层绘制,那么这里先用底层绘制的方法来实现,核心主要是计算之前getItemOffsets
分配的空间的具体位置,并在相应的位置内绘制出对应的内容代码如下:@Override public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View atView = parent.getChildAt(i); if (atView != null) { int position = parent.getChildAdapterPosition(atView); if (position != -1 && (position == 0 || !mCouponBeanList.get(position).getIsUse().equals(mCouponBeanList.get(position - 1).getIsUse()))) { float startY = atView.getTop() - (rectHeight - textHeight) / 2.0f - mTextPaint.getFontMetrics().bottom; canvas.drawText(titles[position == 0 ? 0 : 1], marginX, startY, mTextPaint); } } } }
对于
position != -1
上面已经讲过了,同样的atView != null
的判断,我们点进源码来看如下:
所以我们保险起见对其进行判断,剩下的就是计算得到对应的文字绘制的基线atView.getTop() - (rectHeight - textHeight) / 2.0f - mTextPaint.getFontMetrics().bottom
,这样就完成了,我们运行看一下效果:
-
onDrawOver
顶层绘制来实现效果,其实逻辑都是一样的,只是绘制的顺序先后问题,顶层绘制的图层
复合在最上面,底层绘制的图层
复合在最下面,ItemView绘制的图层
在中间,然而不管怎么样,它们都是将对应的分类Title
绘制到预留出来的空间中,并不会被itemView
覆盖住,所以效果一致,代码如下:@Override public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View atView = parent.getChildAt(i); if (atView != null) { int position = parent.getChildAdapterPosition(atView); if (position != -1 && (position == 0 || !mCouponBeanList.get(position).getIsUse().equals(mCouponBeanList.get(position - 1).getIsUse()))) { float startY = atView.getTop() - (rectHeight - textHeight) / 2.0f - mTextPaint.getFontMetrics().bottom; canvas.drawText(titles[position == 0 ? 0 : 1], marginX, startY, mTextPaint); } } } }
运行看一下效果:
如图,同样达到了我们想要的效果!
三、源码
由于其他布局文件之类和前面两篇都差不多所以这里只贴出我们的
activity
中的测试代码和itemDecoration
代码
-
activity中的代码:
/** * 使用给RecyclerView添加ItemDecoration并在其中绘制分类信息 */ private void showRecyclerViewByItemDecoration() { listView.setVisibility(View.GONE); List<CouponBean> couponBeans = LocalDataServer.requestDateFromServerByItemDecoration(); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.addItemDecoration(new TypeItemDecoration(this, couponBeans, 0x12)); recyclerView.setAdapter(new ItemDecorationRecyclerAdapter(couponBeans, this)); }
-
ItemDecoration
代码:public class TypeItemDecoration extends RecyclerView.ItemDecoration { /** * outRect高度 */ private final int rectHeight; /** * 文字画笔 */ private final TextPaint mTextPaint; /** * X轴方向的距离 */ private final int marginX; /** * Adapter数据 */ private List<CouponBean> mCouponBeanList; /** * 标题 */ private final String[] titles = {"当前可用", "不满足条件"}; /** * 文字高度 */ private final int textHeight; public TypeItemDecoration(Context context, List<CouponBean> couponBeanList, int textSize) { super(); this.mCouponBeanList = couponBeanList; this.rectHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0x32, context.getResources().getDisplayMetrics()); this.marginX = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0x0f, context.getResources().getDisplayMetrics()); this.mTextPaint = new TextPaint(); this.mTextPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize, context.getResources().getDisplayMetrics())); this.mTextPaint.setColor(Color.BLACK); this.textHeight = (int) (mTextPaint.getFontMetrics().bottom - mTextPaint.getFontMetrics().top); } // @Override // public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { // int childCount = parent.getChildCount(); // for (int i = 0; i < childCount; i++) { // View atView = parent.getChildAt(i); // if (atView != null) { // int position = parent.getChildAdapterPosition(atView); // if (position != -1 && (position == 0 || !mCouponBeanList.get(position).getIsUse().equals(mCouponBeanList.get(position - 1).getIsUse()))) { // float startY = atView.getTop() - (rectHeight - textHeight) / 2.0f - mTextPaint.getFontMetrics().bottom; // canvas.drawText(titles[position == 0 ? 0 : 1], marginX, startY, mTextPaint); // } // } // // } // } @Override public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View atView = parent.getChildAt(i); if (atView != null) { int position = parent.getChildAdapterPosition(atView); if (position != -1 && (position == 0 || !mCouponBeanList.get(position).getIsUse().equals(mCouponBeanList.get(position - 1).getIsUse()))) { float startY = atView.getTop() - (rectHeight - textHeight) / 2.0f - mTextPaint.getFontMetrics().bottom; canvas.drawText(titles[position == 0 ? 0 : 1], marginX, startY, mTextPaint); } } } } /** * position == mCouponBeanList.size() - 1 ? rectHeight : 0 * 这是为了判断当前item是否是当前类的最后一个数据且是当前集合中最后一个数据,是则给底部留出一个title个高度显得更加美观 * * @param outRect * @param view * @param parent * @param state */ @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { int position = parent.getChildAdapterPosition(view); if (position != -1) { if (position == 0) { outRect.set(marginX, rectHeight, marginX, position == mCouponBeanList.size() - 1 ? rectHeight : 0); } else if (!mCouponBeanList.get(position).getIsUse().equals(mCouponBeanList.get(position - 1).getIsUse())) { outRect.set(marginX, rectHeight, marginX, position == mCouponBeanList.size() - 1 ? rectHeight : 0); } else { outRect.set(marginX, rectHeight >> 1, marginX, position == mCouponBeanList.size() - 1 ? rectHeight : 0); } } } }
至此三种实现分类显示的方法都已经完成了,如果小伙伴们有更好的办法可以留言,大家一起思考,如果有不正确或者不严谨的地方,欢迎指出!