美文网首页
RecyclerView动态设置分割线

RecyclerView动态设置分割线

作者: OhIAm | 来源:发表于2021-07-21 21:52 被阅读0次

    1.前言

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

    image.png

    2.有问题的实现

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

    //RecyclerView所在的布局
    # activity_main.xml
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/mRv"
            android:layout_marginTop="20dp"
            android:layout_width="0dp"
            android:padding="1dp"
            android:layout_height="wrap_content"
            android:background="@drawable/rv_bg"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    //间隔的xml实现,宽度20dp,颜色透明
    #item_divider_shape.xml
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <size android:width="20dp" />
        <solid android:color="@android:color/transparent" />
    </shape>
    
    #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<String>()
            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
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <size android:width="20dp" />
        <solid android:color="@android:color/holo_purple" />
    </shape>
    
    紫色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<String>()
            for (i in 0 until 5) {
                list.add("txt $i")
            }
            adapter.mData = list
            adapter.notifyDataSetChanged()
        }
    }
    

    5.最后

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

    相关文章

      网友评论

          本文标题:RecyclerView动态设置分割线

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