The use of ItemDecoration in RecyclerView

Usage of RecyclerView

I believe that everyone is already familiar with RecyclerView. People who have used it all praise its powerful functions and excellent performance. It is mainly due to its excellent underlying design and complex caching mechanism. Use the intro now!

layout file

Used to determine the position and size of RecyclerView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".RecyclerViewTestActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/testRV"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Adapter

During the use of RecyclerView, we need to define our own Adapter to adapt RecyclerView to manage data and generate corresponding ItemView

class MyAdapter(val context: Context, private val list: MutableList<NameBean>) :
    RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
    
    

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
    
    
        //加载每个item的布局文件从而生成ViewHolder
        return MyViewHolder(LayoutInflater.from(context).inflate(R.layout.layout_rv_item, parent, false))
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    
    
        holder.update(position)
    }

    override fun getItemCount(): Int {
    
    
        return if (list.isNullOrEmpty()) 0 else list.size
    }

    inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    
    
        //ViewHolder负责UI的刷新,并且点击事件也可以在这里处理
        private val textView = itemView.findViewById<TextView>(R.id.rv_item_tv)
        fun update(position: Int) {
    
    
            textView.text = list[position].name
        }
    }
}

Activity

The parts are built, and then simply assemble our list to be fully realized

class RecyclerViewTestActivity : AppCompatActivity() {
    
    

    private lateinit var mRecyclerView: RecyclerView
    private lateinit var mDataList: MutableList<NameBean>
    private var groupIdStartIndex = 0

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recycler_view_test)
        mRecyclerView = findViewById(R.id.testRV)
        genData()
        mRecyclerView.layoutManager = LinearLayoutManager(this)
        mRecyclerView.adapter = MyAdapter(this, mDataList)
    }

    /**
     * 生成一个数据列表
     */
    private fun genData() {
    
    
        val mutableList: MutableList<NameBean> = mutableListOf()
        for (i in 0 until 100) {
    
    
            when (i) {
    
    
                in 0 until 30 -> {
    
    
                    if (i == 0) {
    
    
                        groupIdStartIndex++
                    }
                    mutableList.add(NameBean("这是第${i}本武林秘籍", "初级 lag", groupIdStartIndex))
                }
                in 30 until 50 -> {
    
    
                    if (i == 30) {
    
    
                        groupIdStartIndex++
                    }
                    mutableList.add(NameBean("这是第${i}本武林秘籍", "中级 lag", groupIdStartIndex))
                }
                in 50 until 75 -> {
    
    
                    if (i == 50) {
    
    
                        groupIdStartIndex++
                    }
                    mutableList.add(NameBean("这是第${i}本武林秘籍", "高级 lag", groupIdStartIndex))
                }
                in 75 until 90 -> {
    
    
                    if (i == 75) {
    
    
                        groupIdStartIndex++
                    }
                    mutableList.add(NameBean("这是第${i}本武林秘籍", "顶级 lag", groupIdStartIndex))
                }
                in 90 until 100 -> {
    
    
                    if (i == 90) {
    
    
                        groupIdStartIndex++
                    }
                    mutableList.add(NameBean("这是第${i}本武林秘籍", "终级 lag", groupIdStartIndex))
                }
            }
        }
        mDataList = mutableList
    }
}

Effect

List of RecyclerViews

Dividing line

The above implements the simplest list. In fact, RecyclerView has many usages, such as setting different layout managers, creating different ViewHolders according to different ViewTypes returned by data, and exploring the rest of the functions by yourself. Next is our focus. Now that there is a list, the list needs a dividing line in many cases. How to realize the dividing line?
There is such a method in RecyclerView, which can add decoration to each of our items. The class definition of ItemDecoration is abstract. Its annotation means that we can draw dividers, highlights, visual grouping boundaries, etc. between items of RecyclerView. Since it is defined as abstract, we need to implement it ourselves. At the same time, the official also implemented a DividerItemDecoration for us to achieve the dividing line effect.

public void addItemDecoration(@NonNull ItemDecoration decor) {
    
    
    addItemDecoration(decor, -1);
}
/**
 * An ItemDecoration allows the application to add a special drawing and layout offset
 * to specific item views from the adapter's data set. This can be useful for drawing dividers
 * between items, highlights, visual grouping boundaries and more.
 *
 * <p>All ItemDecorations are drawn in the order they were added, before the item
 * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
 * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
 * RecyclerView.State)}.</p>
 */
public abstract static class ItemDecoration {
    
    ......}

Use DividerItemDecoration

use the default effect

mRecyclerView.addItemDecoration(DividerItemDecoration(this,LinearLayout.VERTICAL))

You can also customize the style of the dividing line. There is a Drawable named mDivider inside DividerItemDecoration, which provides a Set method to set the style we want

private Drawable mDivider;

public void setDrawable(@NonNull Drawable drawable) {
    
    
    if (drawable == null) {
    
    
        throw new IllegalArgumentException("Drawable cannot be null.");
    }
    mDivider = drawable;
}

Now that it achieves the effect of dividing lines, how does it achieve it? In fact, looking at the source code, its principle is relatively easy to understand. Let's look at two key methods, which are the method of implementing RecyclerView.ItemDecoration. getItemOffsets realizes how much appropriate space needs to be left for each Item, and onDraw The method draws what content in the appropriate space, so the dividing line effect is realized.

@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) {
    
    
    	// 如果是垂直的且分割线不为空,那么设置每个item的底部留出合适的高度
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
    } else {
    
    
    	// 如果是水平的且分割线不为空,那么设置每个item的右边留出合适的高度
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
    }
}

@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);
    }
}

Custom ItemDecoration

First, customize the class and inherit RecyclerView.ItemDecoration. We can see that it actually has two groups of methods, but one of them has been abandoned and has the same name, so we can implement the existing one. Let’s take a look at each What are the meanings of the parameters in each method!

class MyDividerItemDecoration(val context: Context) : RecyclerView.ItemDecoration() {
    
    

    /**
     * @param c:画布,我们可以将该画布从z轴方向上理解为ItemView下边的那一层画布,因此它绘制的内容如果在ItemView的范围之内是不会显示的
     * @param parent:我们的RecyclerView
     * @param state: 包含有关当前RecyclerView状态的有用信息,如目标滚动位置或视图焦点。
     */
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    
    
        super.onDraw(c, parent, state)
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    
    
        super.onDrawOver(c, parent, state)
    }

    /**
     * @param outRect:可以理解为ItemView周围的区域,可以设置它的上下左右
     */
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
    
    
        super.getItemOffsets(outRect, view, parent, state)
    }
}

According to the Api method provided above, we can directly customize ItemDecoration, the specific implementation is as follows:

class PinnedSectionItemDecoration(val context: Context, private val callback: PinnedSectionCallBack) :
    RecyclerView.ItemDecoration() {
    
    

    private val mPaint by lazy {
    
    
        Paint().apply {
    
    
            color = Color.parseColor("#FFAAFF")
        }
    }

    private val mTextPaint by lazy {
    
    
        TextPaint().apply {
    
    
            isAntiAlias = true
            textAlign = Paint.Align.LEFT
            color = Color.parseColor("#BBBBBB")
            textSize = 20F
        }
    }

    /**
     * 分组section的高度
     */
    private val topGap = DensityUtil.dip2px(context, 18F)
    private val sectionLeftPadding = DensityUtil.dip2px(context, 12F)

    /**
     * 在RecyclerView的ViewHolder提供的View的上下左右开辟出区域以供onDraw进行绘制
     */
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
    
    
        super.getItemOffsets(outRect, view, parent, state)
        val pos = parent.getChildAdapterPosition(view)
        val groupId = callback.getGroupId(pos)
        if (groupId < 0)
            return
        if (pos == 0 || isFirstInGroup(pos)) {
    
    
            outRect.top = topGap
        } else {
    
    
            outRect.top = 0
        }
    }

    /**
     * 绘制在RecyclerView之上,也可以绘制在内容的上面,覆盖内容
     */
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    
    
        super.onDrawOver(c, parent, state)
        val itemCount = state.itemCount
        val childCount = parent.childCount
        val left = parent.paddingLeft.toFloat()
        val right = parent.width - parent.paddingRight.toFloat()
        var preGroupId: Int
        var groupId = -1

        for (i in 0 until childCount) {
    
    
            val view = parent.getChildAt(i)
            val position = parent.getChildAdapterPosition(view)
            preGroupId = groupId
            groupId = callback.getGroupId(position)
            if (groupId < 0 || groupId == preGroupId) continue

            val groupTag = callback.getGroupTag(position)
            if (groupTag.isEmpty()) continue

            var textY = topGap.coerceAtLeast(view.top).toFloat()
            if (position + 1 < itemCount) {
    
    
                val nextGroupId = callback.getGroupId(position + 1)
                if (nextGroupId != groupId && view.bottom < textY) {
    
    
                    textY = view.bottom.toFloat()
                }
            }
            c.drawRect(left, textY - topGap, right, textY, mPaint)

            //画文字,需要处理偏移量与baseline才能正确绘制在section的垂直居中
            val textRect = Rect()
            mTextPaint.getTextBounds(groupTag, 0, groupTag.length, textRect)
            val dy =
                (mTextPaint.fontMetricsInt.bottom - mTextPaint.fontMetricsInt.top) /
                        2 - mTextPaint.fontMetricsInt.bottom
            val baseLine = topGap / 2 + dy.toFloat()
            c.drawText(groupTag, left + sectionLeftPadding, (textY - topGap) + baseLine, mTextPaint)
        }
    }

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    
    
        super.onDraw(c, parent, state)
        val left = parent.paddingLeft.toFloat()
        val right = parent.width - parent.paddingRight.toFloat()
        val childCount = parent.childCount
        for (i in 0 until childCount) {
    
    
            val view = parent.getChildAt(i)
            val position = parent.getChildAdapterPosition(view)
            val groupId = callback.getGroupId(position)
            if (groupId < 0) return
            val groupTag = callback.getGroupTag(position)
            if (position == 0 || isFirstInGroup(position)) {
    
    
                val top = view.top - topGap.toFloat()
                val bottom = view.top.toFloat()
                //画背景
                c.drawRect(left, top, right, bottom, mPaint)

                //画文字,需要处理偏移量与baseline才能正确绘制在section的垂直居中
                val textRect = Rect()
                mTextPaint.getTextBounds(groupTag, 0, groupTag.length, textRect)
                val dy =
                    (mTextPaint.fontMetricsInt.bottom - mTextPaint.fontMetricsInt.top) /
                            2 - mTextPaint.fontMetricsInt.bottom
                val baseLine = (top + bottom) / 2 + dy.toFloat()
                c.drawText(groupTag, left + sectionLeftPadding, baseLine, mTextPaint)
            }
        }
    }

    /**
     * 判断是否是同组第一个
     */
    private fun isFirstInGroup(pos: Int): Boolean {
    
    
        if (pos == 0) return true
        //如果上一个的item的groupId与当前的groupId不相同则为第一个
        return callback.getGroupId(pos - 1) != callback.getGroupId(pos)
    }

    interface PinnedSectionCallBack {
    
    

        fun getGroupId(position: Int): Int

        fun getGroupTag(position: Int): String
    }
}

When we instantiate PinnedSectionItemDecoration, we need to pass in a callback. We can implement PinnedSectionCallBack, which provides the group id and group label, and only needs to group the data.

Guess you like

Origin blog.csdn.net/weixin_42643321/article/details/128649186