Android RecycleView 吸顶功能 支持LinearLayoutManager,GridLayoutManager,StaggeredGridLayoutManager

         RecycleView吸顶功能,从网络上随便下载了一张图,类似于下图这种,


具体怎么做呢。其实可以通过 自定义 :RecyclerView.ItemDecoration  实现

先说 RecyclerView.ItemDecoration  里面常用的方法:

getItemOffsets() ---  设置 item 的间距,这个方法比较常用
onDrawOver() --- 绘制在item之上的,就是用这个来控制吸顶
onDraw() --- 绘制 item 里面的内容

这么讲不好理解,直接上图,图片也是在网上扣的,

其实就是在对应标题的 Item ,绘制在其上面。然后我们不管当前Item是否是标题类型,我们都把它绘制在屏幕顶部,也就是 top = 0 的地方,这样就有一块东西是固定在列表顶部显示。

然后如果新的标题即将推到列表顶部,此时就需要把就旧的标题推出屏幕,把 top = 负数,慢慢推出屏幕顶部。

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

先上 Activity 和 对应 xml 的代码

<?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)
    }

}

吸顶功能就是通过自定义 ItemDecoration吸顶实现,其他的,和平常用RecycleView没区别。

当然,我顺便也贴一下Adapter 和 ViewHolder 的代码。

/** 
 * 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  代码
<?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 代码
<?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>

上面贴了这么多代码,都是平常用到的平平无奇的。

而实现吸顶的关键,就是自定义 RecyclerView.ItemDecoration 的实现,来,我们看下里面怎么实现。

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
    }
}

具体原理和实现,代码和注释已经很清晰了。

运行起来,看结果

没问题!!!!

猜你喜欢

转载自blog.csdn.net/Leo_Liang_jie/article/details/122635098