Android RecycleView ceiling function supports LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager

         RecycleView ceiling function, downloaded a picture from the Internet, similar to the picture below,


How to do it.   In fact, it can be achieved by customizing: RecyclerView.ItemDecoration

Let me talk about the commonly used methods in RecyclerView.ItemDecoration : 

getItemOffsets() --- Set the spacing of the item, this method is more commonly used
onDrawOver() --- drawn on top of the item, this is used to control the ceiling
onDraw() --- Draw the content inside the item

It’s not easy to understand, just upload the picture directly, the picture is also deducted from the Internet,

In fact, it is drawn on the Item corresponding to the title. Then we draw it on the top of the screen, where top = 0, regardless of whether the current Item is a title type or not, so that one thing is fixed on the top of the list.

Then if the new title is about to be pushed to the top of the list, you need to push the old title out of the screen at this time, set top = a negative number, and slowly push it out of the top of the screen.

But..........talk is cheap,show u my code

First go to the Activity and the corresponding xml code

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

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycleView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
class StickyTopActivity : AppCompatActivity() {

    companion object {
        fun launch(context: Context) {
            context.startActivity<StickyTopActivity>()
        }
    }

    /**
     *  自定义数据类型
     *  @param type 1为标题,2位普通类型
     * */
    data class MyData(val title: String, val type: Int, val typeTitle: String)

    private val items: ArrayList<MyData> = arrayListOf()

    private fun initData() {
        for (index in 0..1000) {
            if (index % 10 == 0) {
                // index 为 10的倍数,为标题
                items.add(MyData("标题 ${index / 10}", 1, "标题 ${index / 10}"))
            } else {
                // 其他的为普通内容
                items.add(MyData("内容${index}", 2, "标题 ${index / 10}"))
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sticky_top)
        initData()
        recycleView.layoutManager = LinearLayoutManager(this)
        /**
         * 自定义 ItemDecoration,实现RecycleView 吸顶
         * */ 
        recycleView.addItemDecoration(TitleItemDecoration(this, object :
            TitleItemDecoration.TitleDecorationCallback {

            override fun isHeadItem(position: Int) = items[position].type == 1

            override fun getHeadTitle(position: Int) = items[position].typeTitle

        }))
        recycleView.adapter = StickyTopAdapter(items)
    }

}

The ceiling function is achieved by customizing the ItemDecoration ceiling. Others, there is no difference between the usual RecycleView.

Of course, I will also post the code of Adapter and ViewHolder by the way.

/** 
 * Description: Adapter 代码
 */
class StickyTopAdapter(private val items: ArrayList<StickyTopActivity.MyData>) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): RecyclerView.ViewHolder {
        return if (viewType == 1) {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_head, parent, false)
            MyHeadViewHolder(
                view
            )
        } else {
            MyViewHolder(
                LayoutInflater.from(parent.context)
                    .inflate(R.layout.item_content, parent, false)
            )
        }
    }

    override fun getItemCount() = items.size

    override fun getItemViewType(position: Int) = items[position].type

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is MyViewHolder) {
            holder.bind(items[position])
        } else if (holder is MyHeadViewHolder) {
            holder.bind(items[position])
        }
    }
}


/**
 * Description: HeadViewHolder 标题ViewHolder
 */
class MyHeadViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val textView = itemView.findViewById<TextView>(R.id.tvTitle)
    fun bind(data: StickyTopActivity.MyData) {
        textView.text = data.title
    }
}


/**
 * Description: 普通ViewHolder
 */
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val textView = itemView.findViewById<TextView>(R.id.textView)
    fun bind(data: StickyTopActivity.MyData) {
        textView.text = data.title
    }
}

item_head.xml code
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="44dp"
    android:background="#afefdd"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/tvTitle"
        android:layout_width="200dp"
        android:layout_height="44dp"
        android:gravity="center_vertical|left"
        android:textSize="14sp"
        android:textColor="#000000"
        android:paddingLeft="25dp"
        android:text="123"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
item_content.xml code
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#80475961"
        android:gravity="center"
        android:layout_margin="10dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

There are so many codes posted on it, all of which are ordinary and unremarkable.

The key to achieve the ceiling is to customize the implementation of RecyclerView.ItemDecoration , come, let's see how to implement it.

class TitleItemDecoration(
    private val context: Context,
    private val callback: TitleDecorationCallback
) : RecyclerView.ItemDecoration() {

    private val mTitleHeight: Int

    // 直接引用标题ViewHolder对应的layout -- item_head.xml
    private val titleLayout: View = LayoutInflater.from(context).inflate(R.layout.item_head, null)
    private val tvTitle = titleLayout.findViewById<TextView>(R.id.tvTitle)

    init {
        /**
         * 手动测量头部所需高度,这里提醒一下,item_head.xml 这个layout文件里的控件的宽高,不要用 wrap_content 和 match_parent
         * 要明确标出宽高,才能测量出来,至于为什么,这个就得自己去探究自定义View 测量绘制的原理了,不是此文所介绍的,这里只做提醒
         * */
        titleLayout.measure(View.MeasureSpec.AT_MOST, View.MeasureSpec.UNSPECIFIED)
        mTitleHeight = titleLayout.measuredHeight
    }

    /**
     *      重写 onDrawOver()
     * */
    override fun onDrawOver(
        canvas: Canvas,
        recyclerView: RecyclerView,
        state: RecyclerView.State
    ) {
        super.onDrawOver(canvas, recyclerView, state)
        // 获取第一个可见 Item 对应的 Position
        val firstVisiblePosition = findFirstVisibleItemPosition(recyclerView.layoutManager!!)
        if (firstVisiblePosition <= -1 || firstVisiblePosition >= recyclerView.adapter!!.itemCount - 1) {
            // 安全检测,防止越界
            return
        }
        // 获取第一个可见 Item 对应 View
        val firstVisibleView =
            recyclerView.findViewHolderForAdapterPosition(firstVisiblePosition)!!.itemView

        // 因为我们要绘制在列表顶部,所以先获取RecycleView 左右上 三个坐标
        val left = recyclerView.paddingLeft
        val right = recyclerView.width - recyclerView.paddingRight
        var top = recyclerView.paddingTop
        
        /**
         * 这里要判断,我们下一行是否是标题,如果是,原来绘制在屏幕上的标题,就得推出屏幕顶部
         * 至于推出屏幕顶部距离多少,就得看下一个标题已经推进吸顶区域大多
         * 下面就是获取下一个标题推进吸顶区域的高度是多大
         * */ 
        if (nextLineIsTitle(
                firstVisibleView,
                firstVisiblePosition,
                recyclerView
            ) && firstVisibleView.bottom < mTitleHeight
        ) {
            top = if (mTitleHeight <= firstVisibleView.height) {
                val d = firstVisibleView.height - mTitleHeight
                /**
                 * 通常来说,这里这个d 是等于0的,因为吸顶区域的高度一般都会和列表里面的标题的高度是一模一样的
                 * firstVisibleView.top 就是第一个可见Item 的顶部,这里的top如果是负数,即说明 firstVisibleView已经有一部分
                 * 滑出屏幕了,这时候吸顶绘制的区域,也要跟随它
                 * */
                firstVisibleView.top + d
            } else {
                val d = mTitleHeight - firstVisibleView.height
                firstVisibleView.top - d
            }
        }
        // 去绘制头部
        drawTitle(canvas, top, firstVisiblePosition, left, right)
    }

    private fun drawTitle(canvas: Canvas, top: Int, position: Int, left: Int, right: Int) {
        // 设置偏移,dx=0,即代表向左对齐
        canvas.translate(0f, top.toFloat())
        tvTitle.text = callback.getHeadTitle(position)
        titleLayout.layout(left, 0, right, mTitleHeight)
        titleLayout.draw(canvas)
    }

    
    private fun findFirstVisibleItemPosition(layoutManager: RecyclerView.LayoutManager): Int {
        return when (layoutManager) {
            is LinearLayoutManager -> {
                layoutManager.findFirstVisibleItemPosition()
            }
            is GridLayoutManager -> {
                layoutManager.findFirstVisibleItemPosition()
            }
            is StaggeredGridLayoutManager -> {
                layoutManager.findFirstVisibleItemPositions(null)[0]
            }
            else -> {
                throw RuntimeException("咱不支持 类型为:${layoutManager.javaClass.name} 的LayoutManager ,可以自己判断类型,转成自己的LayoutManager,去获取第一个可见Item的position ")
            }
        }
    }

    /**
     *      网格布局应该算下一行是否是Title,而不是算下一个Position
     *      @param 当前Item
     *      @param 当前position
     *      @param parent
     * */
    private fun nextLineIsTitle(
        currentView: View,
        currentPosition: Int,
        parent: RecyclerView
    ): Boolean {
        for (nextLinePosition in currentPosition + 1 until parent.adapter!!.itemCount) {
            val nextItemView = parent.findViewHolderForAdapterPosition(nextLinePosition)!!.itemView
            if (nextItemView.bottom > currentView.bottom) {
                // 找到下一行的 Position
                return callback.isHeadItem(nextLinePosition)
            }
        }
        return false
    }

    interface TitleDecorationCallback {
        /**
         *      当前 position 对应的ViewHolder 是否是标题类型
         * */
        fun isHeadItem(position: Int): Boolean

        /**
         *      当前 position 对应的ViewHolder 是属于哪一种标题类型
         * */
        fun getHeadTitle(position: Int): String
    }
}

The specific principle and implementation, code and comments are already very clear.

Run it and see the result

no problem! ! ! !

Guess you like

Origin blog.csdn.net/Leo_Liang_jie/article/details/122635098