Android分类列表之RecyclerView-ItemDecoration实现

Android分类列表之RecyclerView-ViewType实现

前两篇不管是ListView还是RecyclerView都是根据ViewType来实现的,是将分类标题看成分类的一种来建模实现的,本篇则使用RecyclerView中ItemDecoration来实现分类的标题显示,如果对本篇有点晕的还请先移步到前两篇文章,传送门:Android分类列表之ListView-ViewType实现Android分类列表之RecyclerView-ViewType实现

一、分析思路

由于一些代码逻辑和布局和前两篇是一致的,所以接下来涉及到这些重复的代码区域将会简单的一笔带过,重点放到我们的核心ItemDecoration中

  1. 创建RecyclerView适配器ItemDecorationRecyclerAdapter,因为前两篇都是根据ViewType来实现分类列表的,所以会有两种布局当然也就有两种ViewHolder了(CouponViewHolderTitleViewHolder),而使用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());
            }
        }
    }
    
  2. 关于RecyclerViewItemDecoration,是Google为了更灵活的给RecyclerViewitem之间添加一些更炫酷可自定义的内容,最简单的就是添加分割线条,在创建完ItemDecoartion后通过recyclerView.addItemDecoration()将其添加到recyclerView中,我们先看一下ItemDecoration吧。
    1)ItemDecorationRecyclerView中的一个静态的抽象类如下:

        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来实现

  1. 创建类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);
        }
    
    }
    
  2. 重写方法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>
运行效果图
  1. 绘制分类标题,上面分析过,我们可以分别采用底层绘制和顶层绘制,那么这里先用底层绘制的方法来实现,核心主要是计算之前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,这样就完成了,我们运行看一下效果:
    在这里插入图片描述

  2. 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代码

  1. 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));
        }
    
    
  2. 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);
                }
            }
    
        }
    
    }
    
    

至此三种实现分类显示的方法都已经完成了,如果小伙伴们有更好的办法可以留言,大家一起思考,如果有不正确或者不严谨的地方,欢迎指出!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值