RecyclerView动态设置分割线

1.前言

RecyclerView的item很多情况下都是需要有分割线的或者说是彼此之间需要有间隔。
如下图示例,每个item大小一致,假设彼此之间的分割线宽度为20dp,分割线是透明的。那么此时分割线的作用更多是作为item之间的间隔。

image.png

2.有问题的实现

通常我们的做法就是在RecyclerView中添加DividerItemDecoration,这是实现了RecyclerView库中帮我们默认实现了ItemDecoration这个抽象类的一个类。使用方法就是通过setDrawable(@NonNull Drawable drawable)方法设置分割线的样式。

//RecyclerView所在的布局
# activity_main.xml
    

//间隔的xml实现,宽度20dp,颜色透明
#item_divider_shape.xml

    
    


#MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

     val adapter = ItemAdapter()
        mRv.adapter = adapter
        mRv.layoutManager = LinearLayoutManager(baseContext, RecyclerView.HORIZONTAL, false)
        mRv.addItemDecoration(
            DividerItemDecoration(
                baseContext,
                DividerItemDecoration.HORIZONTAL
            ).apply {
                setDrawable(resources.getDrawable(R.drawable.item_divider_shape))
            })
        val list = mutableListOf()
        for (i in 0 until 5) {
            list.add("txt $i")
        }
        adapter.mData = list
        adapter.notifyDataSetChanged()    }
}

上面的做法是可以实现item之间的透明分割线,但是会有一个问题,就是最后一个item也有20dp的间隔。如下图,滚动到尽头时,发现最后一个item也有了分割线。


最后一个item也加上了分割线.png

3.有点麻烦的实现

如果我们想要去掉最后一个item的分割线,网上搜索到做法很多都是继承DividerItemDecoration,再自己重写DividerItemDecorationonDraw里的相关方法。

#DividerItemDecoration.java
@Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() == null || mDivider == null) {
            return;
        }
        if (mOrientation == VERTICAL) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

private void drawHorizontal(Canvas canvas, RecyclerView parent) {
        ...
//在这里遍历所有子view然后一个个画出分割线
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
            final int right = mBounds.right + Math.round(child.getTranslationX());
            final int left = right - mDivider.getIntrinsicWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        ...
    }

上面的遍历的时候,我们可以想到的就是,把for循环的childCount减1,这样最后一个item的分割线就不会画出来了。

private void drawHorizontal(Canvas canvas, RecyclerView parent) {
        ...

//在这里遍历所有子view然后一个个画出分割线
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount -1;  i++) {//这里减了1,最后一个child的分割线就不画出来
            final View child = parent.getChildAt(i);
            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
            final int right = mBounds.right + Math.round(child.getTranslationX());
            final int left = right - mDivider.getIntrinsicWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }

!!!然而,这种做法是没用的。你还是会看到最后一个item是有间隔的。为什么最后一个item还是会间隔呢?我们不是不让它画出来了吗。实际这个divider的的确确没有被画出来。改一下divider的颜色就可以看到,最后一个divider的确没有被画出来。

//间隔的xml实现,宽度20dp,颜色紫色
#item_divider_shape.xml

    
    

紫色divider.png

为什么最后多出来一些,因为这是多出来的宽度。是什么宽度呢?是这个RecyclerView需要的宽度,我们绘制之前肯定是先要测量确定大小。所以问题可能出在了测量的时候,测量把所有的item和所有的divider的宽度都计算进去了

我们再看一下ItemDecoration这个类里的方法,除了draw相关的,我们还可以看到一个方法getItemOffsets,查看这个方法的调用,可以看到RecyclerView里的一个方法调用到了getItemOffsets。里面的代码是说什么呢,我们分析得出:它就是用传进来的child来获取ItemDecoration设置给这个child的大小。

#RecyclerView.java
 Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }

        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            //这里可以获取到设置给ItemDecoration的drawable的大小。 
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

再追踪这个getItemDecorInsetsForChild(View child),我们发现它有被RecyclerView的内部类LayoutManagermeasureChild调用到。
我们知道父View肯定遍历每个子View进行测量的,所以最后一个item的divider的大小也是有算进去了。所以才会出现没有绘制divider,却出现了divider的情况。

#RecyclerView$LayoutManager.java
        public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

          //这里拿到了ItemDecoration的大小
            final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
            widthUsed += insets.left + insets.right;
            heightUsed += insets.top + insets.bottom;
            final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                    getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
                    canScrollHorizontally());
            final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                    getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
                    canScrollVertically());
            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
                child.measure(widthSpec, heightSpec);
            }
        }

到了这里,难道要重写RecyclerView的测量方法,那肯定不是。回到ItemDecorationgetItemOffsets,即然divider的大小需要调用它来计算得到,那么我们是否可以重写这个方法,答案是可以的。判断是不是最后一个item,是的话就设置为0,这样就解决了。

@Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
                               RecyclerView.State state) {
        if (mDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        if (mOrientation == VERTICAL) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            int adapterPosition = parent.getChildAdapterPosition(view);
//判断是不是最后一个item,是的话就设置给outRect的数据都设置为0
            if (adapterPosition == state.getItemCount() - 1) {
                outRect.set(0, 0, 0, 0);
            } else {
                outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
            }

        }
    }

image.png

可以看到最后一个item的间隔已经没有了。
但是我们真的需要把整个DividerItemDecoration的全部代码复制出来,再修改drawHorizontalgetItemOffsets这两个方法吗。有没有更简单的方法??
当然有

4.简单的实现

由上面我们知道,如果在调用到getItemOffsets这个方法时,如果不给最后一个item的设置divider的大小,那么最终测量出来的宽度就是不包含最后一个item的divider的宽度,即使最后一个item的divider被画出来,但它没地方显示啊。

#MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val adapter = ItemAdapter()
        mRv.adapter = adapter
        mRv.layoutManager = LinearLayoutManager(baseContext, RecyclerView.HORIZONTAL, false)
      //这里我们重写getItemOffsets的实现即可  
      mRv.addItemDecoration(
            object : DividerItemDecoration(
                baseContext,
                HORIZONTAL
            ) {
                override fun getItemOffsets(
                    outRect: Rect,
                    view: View,
                    parent: RecyclerView,
                    state: RecyclerView.State
                ) {
                  //drawable就是我们的divider
                    drawable?.let {
                        when (parent.getChildAdapterPosition(view)) {
                            state.itemCount - 1 -> {//是不是最后一个,是的话设置divider的宽度为0
                                outRect.set(0, 0, 0, 0)
                            }
                            else -> {
                                outRect.set(0, 0, it.intrinsicWidth, 0)
                            }
                        }
                    }
                }
            }.apply {
                setDrawable(resources.getDrawable(R.drawable.item_divider_shape))
            })
        val list = mutableListOf()
        for (i in 0 until 5) {
            list.add("txt $i")
        }
        adapter.mData = list
        adapter.notifyDataSetChanged()
    }
}

5.最后

记一下笔记,如果有错误帮忙提出,谢谢大家。

你可能感兴趣的:(RecyclerView动态设置分割线)