美文网首页自定义view程序员Android开发
RecyclerView之万能分割线(二)

RecyclerView之万能分割线(二)

作者: 侯蛋蛋_ | 来源:发表于2017-09-18 20:20 被阅读464次

    概述

    RecyclerView(一)使用完全指南
    RecyclerView(二)之万能分割线
    RecyclerView之瀑布流(三)
    上一篇讲解了RecyclerView的基本用法,回顾下上一篇文章讲解内容

    • 水平列表展示,设置LayoutManager的方向性
    • 竖直列表展示,设置LayoutManager的方向性
    • 自定义间隔,RecyclerView.addItemDecoration()
    • Item添加和删除动画,RecyclerView.setItemAnimator()

    关于网格样式和瀑布流样式在本篇会仔细的介绍,细心的同学会发现,自定义间隔在上一篇文章中并没有太过深入,只是介绍了方法的调用时机,但是关于更换间隔样式没有太详细的介绍,是因为列表样式的RecyclerView自定义间隔比较简单,统一放到复杂一点的网格中来讲解。直接进入主题,看看期待已久的网格模式和万能分割线

    网格样式

    上篇文章中已经了解到,RecyclerView展示的样式是有布局管理器LayoutManager来控制。网格样式的管理器是GridLayoutManager,看一下它最常用的两个构造函数以及参数含义。

    • GridLayoutManager(Context context, int spanCount)

    1.spanCount ,每列或者每行的item个数,设置为1,就是列表样式

    2.该构造函数默认是竖直方向的网格样式

    • GridLayoutManager(Context context, int spanCount, int orientation,boolean reverseLayout)
    1. spanCount,每列或者每行的item个数,设置为1,就是列表样式

    2. 网格样式的方向,水平(OrientationHelper.HORIZONTAL[hɒrɪ'zɒnt(ə)l])或者竖直(OrientationHelper.VERTICAL['vɜːtɪk(ə)l])

    3. reverseLayout,是否逆向,true:布局逆向展示,false:布局正向显示

    看一下使用。

    // 竖直方向的网格样式,每行四个Item
    mLayoutManager = new GridLayoutManager(this, 4, OrientationHelper.VERTICAL, false);
    mRecyclerView.setLayoutManager(mLayoutManager);
    
    RecyclerView-网格无间隔.jpg

    网格样式已经显示出来了,和之前遇见的问题一样,没有间隔线,非常丑,间隔线必须加,而且要使用自定义,不使用系统自带的。

    新建文件md_divider.xml,是一个灰色的矩形。

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
           android:shape="rectangle" >
        <solid android:color="@android:color/darker_gray"/>
        <size android:height="4dp" android:width="4dp"/>
    </shape>
    
    

    在styles.xml中的自定义的应用主题里替换掉listdivider属性。

    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:listDivider">@drawable/md_divider</item>
    </style>
    

    然后继承RecyclerView.ItemDecoration类,在构造函数里获取自定义的间隔线,复写绘制间隔线的方法。

    public class MDGridRvDividerDecoration extends RecyclerView.ItemDecoration {
    
        private static final int[] ATTRS = new int[]{
                android.R.attr.listDivider
        };
    
        /**
         * 用于绘制间隔样式
         */
        private Drawable mDivider;
    
        public MDGridRvDividerDecoration(Context context) {
            // 获取默认主题的属性
            final TypedArray a = context.obtainStyledAttributes(ATTRS);
            mDivider = a.getDrawable(0);
            a.recycle();
        }
    
    
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            // 绘制间隔,每一个item,绘制右边和下方间隔样式
            int childCount = parent.getChildCount();
            int spanCount = ((GridLayoutManager)parent.getLayoutManager()).getSpanCount();
            int orientation = ((GridLayoutManager)parent.getLayoutManager()).getOrientation();
            boolean isDrawHorizontalDivider = true;
            boolean isDrawVerticalDivider = true;
            int extra = childCount % spanCount;
            extra = extra == 0 ? spanCount : extra;
            for(int i = 0; i < childCount; i++) {
                isDrawVerticalDivider = true;
                isDrawHorizontalDivider = true;
                // 如果是竖直方向,最右边一列不绘制竖直方向的间隔
                if(orientation == OrientationHelper.VERTICAL && (i + 1) % spanCount == 0) {
                    isDrawVerticalDivider = false;
                }
    
                // 如果是竖直方向,最后一行不绘制水平方向间隔
                if(orientation == OrientationHelper.VERTICAL && i >= childCount - extra) {
                    isDrawHorizontalDivider = false;
                }
    
                // 如果是水平方向,最下面一行不绘制水平方向的间隔
                if(orientation == OrientationHelper.HORIZONTAL && (i + 1) % spanCount == 0) {
                    isDrawHorizontalDivider = false;
                }
    
                // 如果是水平方向,最后一列不绘制竖直方向间隔
                if(orientation == OrientationHelper.HORIZONTAL && i >= childCount - extra) {
                    isDrawVerticalDivider = false;
                }
    
                if(isDrawHorizontalDivider) {
                    drawHorizontalDivider(c, parent, i);
                }
    
                if(isDrawVerticalDivider) {
                    drawVerticalDivider(c, parent, i);
                }
            }
        }
    
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
            int orientation = ((GridLayoutManager)parent.getLayoutManager()).getOrientation();
            int position = parent.getChildLayoutPosition(view);
            if(orientation == OrientationHelper.VERTICAL && (position + 1) % spanCount == 0) {
                outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
                return;
            }
    
            if(orientation == OrientationHelper.HORIZONTAL && (position + 1) % spanCount == 0) {
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
                return;
            }
    
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight());
        }
    
        /**
         * 绘制竖直间隔线
         * 
         * @param canvas
         * @param parent
         *              父布局,RecyclerView
         * @param position
         *              irem在父布局中所在的位置
         */
        private void drawVerticalDivider(Canvas canvas, RecyclerView parent, int position) {
            final View child = parent.getChildAt(position);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getTop() - params.topMargin;
            final int bottom = child.getBottom() + params.bottomMargin + mDivider.getIntrinsicHeight();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
    
        /**
         * 绘制水平间隔线
         * 
         * @param canvas
         * @param parent
         *              父布局,RecyclerView
         * @param position
         *              item在父布局中所在的位置
         */
        private void drawHorizontalDivider(Canvas canvas, RecyclerView parent, int position) {
            final View child = parent.getChildAt(position);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            final int left = child.getLeft() - params.leftMargin;
            final int right = child.getRight() + params.rightMargin + mDivider.getIntrinsicWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
    }
    
    

    设置RecyclerView的间隔线。

    mRecyclerView.addItemDecoration(new MDGridRvDividerDecoration(this));
    

    运行效果如下图。

    关于网格样式的RecyclerView使用大体和列表样式相同,主要在于间隔线的实现上有些不同,来看一下如果真正的使用自定义的间隔线需要做些什么。
    实现间隔线样式,可以是xml文件也可以是图片
    覆盖应用主题的listdivider属性,使用自定义的间隔线样式
    继承RecyclerView.ItemDecoration
    类,并实现其中的绘制间隔线方法
    设置RecyclerView间隔线样式

    关于第三步,实现绘制线的方法,上面的代码提供了一种大体的思路,可以供大家借鉴,下面就让我们看看期待已久的瀑布流样式的列表。
    瀑布流样式
    RecyclerView的瀑布流布局管理器是taggeredGridLayoutManager,它最常用的构造函数就一个,StaggeredGridLayoutManager(int spanCount, int orientation),spanCount代表每行或每列的Item个数,orientation代表列表的方向,竖直或者水平。
    看在代码中的使用。

    其实我们发现这样设置分割线,如果换样式的话就太麻烦了,所以给大家找了万能分割线的代码,用的时候直接复制黏贴就可以了,如果技术过硬的话可以分析一下源码,毕竟也不是很难

    代码使用方法:

    添加水平分割线:高度为2px,颜色为灰色

            //水平分割线
            recyclerView.addItemDecoration(new DividerItemDecoration(
                    ListRecycle.this, DividerItemDecoration.HORIZONTAL_LIST));
    

    添加垂直分割线:高度为2px,颜色为灰色

        //垂直分割线
        recyclerView.addItemDecoration(new DividerItemDecoration(
                    ListRecycle.this, DividerItemDecoration.VERTICAL_LIST));
    

    平+垂直分割线:高度为2px,颜色为灰色

          //垂直+水平分割线
          recyclerView.addItemDecoration(new DividerItemDecoration(
                    ListRecycle.this, DividerItemDecoration.BOTH_SET));
    

    带颜色的分割线

        //添加带颜色分割线
        recyclerView.addItemDecoration(new DividerItemDecoration(
                    ListRecycle.this, DividerItemDecoration.BOTH_SET,5,Color.BLUE));
    

    自定义图片的分割线(图片必须有高度,比如说Xml文件,就可能没设置高度):

        //添加图片分割线
        recyclerView.addItemDecoration(new DividerItemDecoration(
                    ListRecycle.this, DividerItemDecoration.HORIZONTAL_LIST,R.mipmap.ic_launcher));
    

    万能分割线

    /**
     * 万能分割线
     * Created by ChenSS on 2016/9/21.
     */
    public class DividerItemDecoration extends RecyclerView.ItemDecoration {
        private Paint mPaint;
        //取名mDivider似乎更恰当
        private Drawable mDrawable;
        //分割线高度,默认为1px
        private int mDividerHeight = 2;
        //列表的方向
        private int mOrientation;
        //系统自带的参数
        private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
        //水平
        public static final int HORIZONTAL_LIST = RecyclerView.HORIZONTAL;
        //垂直
        public static final int VERTICAL_LIST = RecyclerView.VERTICAL;
        //水平+垂直
        public static final int BOTH_SET = 2;
    
    
        /**
         * 默认分割线:高度为2px,颜色为灰色
         *
         * @param context     上下文
         * @param orientation 列表方向
         */
        public DividerItemDecoration(Context context, int orientation) {
            this.setOrientation(orientation);
            //获取xml配置的参数
            final TypedArray a = context.obtainStyledAttributes(ATTRS);
            //typedArray.getDrawable(attr)这句是说我们可以通过我们的资源获得资源,使用我们的资源名attr去获得资源id
            //看不懂就用自己写一个分割线的图片吧,方法:ContextCompat.getDrawable(context, drawableId);
            mDrawable = a.getDrawable(0);
            //官方的解释是:回收TypedArray,以便后面重用。在调用这个函数后,你就不能再使用这个TypedArray。
            //在TypedArray后调用recycle主要是为了缓存。
            a.recycle();
        }
    
        /**
         * 自定义分割线
         *
         * @param context     上下文
         * @param orientation 列表方向
         * @param drawableId  分割线图片
         */
        public DividerItemDecoration(Context context, int orientation, int drawableId) {
            this.setOrientation(orientation);
            //旧的getDrawable方法弃用了,这个是新的
            mDrawable = ContextCompat.getDrawable(context, drawableId);
            mDividerHeight = mDrawable.getIntrinsicHeight();
        }
    
        /**
         * 自定义分割线
         *
         * @param context       上下文
         * @param orientation   列表方向
         * @param dividerHeight 分割线高度
         * @param dividerColor  分割线颜色
         */
        public DividerItemDecoration(Context context, int orientation,
                                     int dividerHeight, int dividerColor) {
            this.setOrientation(orientation);
            mDividerHeight = dividerHeight;
            Log.e("mDividerHeight", mDividerHeight + "===================");
            //抗锯齿画笔
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint.setColor(dividerColor);
            //填满颜色
            mPaint.setStyle(Paint.Style.FILL);
        }
    
        /**
         * 设置方向
         *
         * @param orientation
         */
        public void setOrientation(int orientation) {
            if (orientation < 0 || orientation > 2)
                throw new IllegalArgumentException("invalid orientation");
            mOrientation = orientation;
        }
    
    
        /**
         * 绘制分割线之后,需要留出一个外边框,就是说item之间的间距要换一下
         *
         * @param outRect outRect.set(0, 0, 0, 0);的四个参数理解成margin就好了
         * @param view    视图
         * @param parent  父级view
         * @param state
         */
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            //下面super...代码其实调用的就是那个过时的getItemOffsets,也就是说这个方法体内容也可以通通移到那个过时的getItemOffsets中
            super.getItemOffsets(outRect, view, parent, state);
            //获取layoutParams参数
            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams();
            //当前位置
            int itemPosition = layoutParams.getViewLayoutPosition();
            //ItemView数量
            int childCount = parent.getAdapter().getItemCount();
            switch (mOrientation) {
                case BOTH_SET:
                    //获取Layout的相关参数
                    int spanCount = this.getSpanCount(parent);
                    if (isLastRaw(parent, itemPosition, spanCount, childCount)) {
                        // 如果是最后一行,则不需要绘制底部
                        outRect.set(0, 0, mDividerHeight, 0);
                    } else if (isLastColum(parent, itemPosition, spanCount, childCount)) {
                        // 如果是最后一列,则不需要绘制右边
                        outRect.set(0, 0, 0, mDividerHeight);
                    } else {
                        outRect.set(0, 0, mDividerHeight, mDividerHeight);
                    }
                    break;
                case VERTICAL_LIST:
                    childCount -= 1;
                    //水平布局右侧留Margin,如果是最后一列,就不要留Margin了
                    outRect.set(0, 0, (itemPosition != childCount) ? mDividerHeight : 0, 0);
                    break;
                case HORIZONTAL_LIST:
                    childCount -= 1;
                    //垂直布局底部留边,最后一行不留
                    outRect.set(0, 0, 0, (itemPosition != childCount) ? mDividerHeight : 0);
                    break;
            }
        }
    
        /**
         * 绘制分割线
         *
         * @param c
         * @param parent
         * @param state
         */
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            if (mOrientation == VERTICAL_LIST) {
                drawVertical(c, parent);
            } else if (mOrientation == HORIZONTAL_LIST) {
                drawHorizontal(c, parent);
            } else {
                drawHorizontal(c, parent);
                drawVertical(c, parent);
            }
        }
    
        /**
         * 绘制横向 item 分割线
         *
         * @param canvas 画布
         * @param parent 父容器
         */
        private void drawHorizontal(Canvas canvas, RecyclerView parent) {
            final int x = parent.getPaddingLeft();
            final int width = parent.getMeasuredWidth() - parent.getPaddingRight();
            //getChildCount()(ViewGroup.getChildCount) 返回的是显示层面上的“所包含的子 View 个数”。
            final int childSize = parent.getChildCount();
            for (int i = 0; i < childSize; i++) {
                final View child = parent.getChildAt(i);
                RecyclerView.LayoutParams layoutParams =
                        (RecyclerView.LayoutParams) child.getLayoutParams();
                //item底部的Y轴坐标+margin值
                final int y = child.getBottom() + layoutParams.bottomMargin;
                final int height = y + mDividerHeight;
                Log.e("height", height + "===================");
                if (mDrawable != null) {
                    //setBounds(x,y,width,height); x:组件在容器X轴上的起点 y:组件在容器Y轴上的起点
                    // width:组件的长度 height:组件的高度
                    mDrawable.setBounds(x, y, width, height);
                    mDrawable.draw(canvas);
                }
                if (mPaint != null) {
                    canvas.drawRect(x, y, width, height, mPaint);
                }
            }
        }
    
        /**
         * 绘制纵向 item 分割线
         *
         * @param canvas
         * @param parent
         */
        private void drawVertical(Canvas canvas, RecyclerView parent) {
            final int top = parent.getPaddingTop();
            final int bottom = parent.getMeasuredHeight() - parent.getPaddingBottom();
            final int childSize = parent.getChildCount();
            for (int i = 0; i < childSize; i++) {
                final View child = parent.getChildAt(i);
                RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
                final int left = child.getRight() + layoutParams.rightMargin;
                final int right = left + mDividerHeight;
                if (mDrawable != null) {
                    mDrawable.setBounds(left, top, right, bottom);
                    mDrawable.draw(canvas);
                }
                if (mPaint != null) {
                    canvas.drawRect(left, top, right, bottom, mPaint);
                }
            }
        }
    
    
        /**
         * 获取列数
         *
         * @param parent
         * @return
         */
        private int getSpanCount(RecyclerView parent) {
            int spanCount = -1;
            RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                spanCount = ((StaggeredGridLayoutManager) layoutManager)
                        .getSpanCount();
            }
            return spanCount;
        }
    
    
        private boolean isLastColum(RecyclerView parent, int pos, int spanCount,
                                    int childCount) {
            RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                int orientation = ((GridLayoutManager) layoutManager)
                        .getOrientation();
                if (orientation == StaggeredGridLayoutManager.VERTICAL) {
                    // 如果是最后一列,则不需要绘制右边
                    if ((pos + 1) % spanCount == 0)
                        return true;
                } else {
                    childCount = childCount - childCount % spanCount;
                    // 如果是最后一列,则不需要绘制右边
                    if (pos >= childCount)
                        return true;
                }
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                int orientation = ((StaggeredGridLayoutManager) layoutManager)
                        .getOrientation();
                if (orientation == StaggeredGridLayoutManager.VERTICAL) {
                    // 如果是最后一列,则不需要绘制右边
                    if ((pos + 1) % spanCount == 0)
                        return true;
                } else {
                    childCount = childCount - childCount % spanCount;
                    // 如果是最后一列,则不需要绘制右边
                    if (pos >= childCount)
                        return true;
                }
            }
            return false;
        }
    
        private boolean isLastRaw(RecyclerView parent, int pos, int spanCount,
                                  int childCount) {
            int orientation;
            RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                childCount = childCount - childCount % spanCount;
                orientation = ((GridLayoutManager) layoutManager)
                        .getOrientation();
                if (orientation == StaggeredGridLayoutManager.VERTICAL) {
                    // 如果是最后一行,则不需要绘制底部
                    childCount = childCount - childCount % spanCount;
                    if (pos >= childCount)
                        return true;
                } else {// StaggeredGridLayoutManager 横向滚动
                    // 如果是最后一行,则不需要绘制底部
                    if ((pos + 1) % spanCount == 0)
                        return true;
                }
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                orientation = ((StaggeredGridLayoutManager) layoutManager)
                        .getOrientation();
                if (orientation == StaggeredGridLayoutManager.VERTICAL) {
                    // 如果是最后一行,则不需要绘制底部
                    childCount = childCount - childCount % spanCount;
                    if (pos >= childCount)
                        return true;
                } else {// StaggeredGridLayoutManager 横向滚动
                    // 如果是最后一行,则不需要绘制底部
                    if ((pos + 1) % spanCount == 0)
                        return true;
                }
            }
            return false;
        }
    }
    

    根据王三的猫阿德加以改编

    相关文章

      网友评论

      • 我一定会学会:大佬一个地方错了:
        switch (mOrientation) {
        case BOTH_SET:
        //获取Layout的相关参数
        int spanCount = this.getSpanCount(parent);
        if (isLastRaw(parent, itemPosition, spanCount, childCount)) {
        // 如果是最后一行,则不需要绘制底部
        outRect.set(0, 0, mDividerHeight, 0);
        } else if (isLastColum(parent, itemPosition, spanCount, childCount)) {
        // 如果是最后一列,则不需要绘制右边
        outRect.set(0, 0, 0, mDividerHeight);
        } else {
        outRect.set(0, 0, mDividerHeight, mDividerHeight);
        }
        break;
        case VERTICAL_LIST:
        childCount -= 1;
        //水平布局右侧留Margin,如果是最后一列,就不要留Margin了
        outRect.set(0, 0, (itemPosition != childCount) ? mDividerHeight : 0, 0);
        break;
        case HORIZONTAL_LIST:
        childCount -= 1;
        //垂直布局底部留边,最后一行不留
        outRect.set(0, 0, 0, (itemPosition != childCount) ? mDividerHeight : 0);
        break;
        }

        需要把 case VERTICAL_LIST和case HORIZONTAL_LIST:对调

      本文标题:RecyclerView之万能分割线(二)

      本文链接:https://www.haomeiwen.com/subject/fnbbsxtx.html