实现RecyclerView二级列表

自定义RecyclerView的adapter实现二级列表
图片大于5MB,CSDN不让上传,使用github链接,如果看不到请使用科学上网
https://github.com/nanjolnoSat/PersonalProject/blob/master/Recyclerexpanableadapter/pic/pic1.gif
源码


必要方法

抽一个base出来,因为不可能每次需要这个功能就把相同代码编写一遍。先提供必要的方法,再思考怎么完善方法的细节。

typealias OnParentClickListener = (parentPosition: Int) -> Unit
typealias OnChildClickListener = (parentPosition: Int, childPosition: Int) -> Unit

abstract class BaseRecyclerExpandableAdapter<PARENT_VH : BaseRecyclerExpandableAdapter.BaseViewHolder, CHILD_VH : BaseRecyclerExpandableAdapter.BaseViewHolder> :
    RecyclerView.Adapter<BaseRecyclerExpandableAdapter.BaseViewHolder>() {

    companion object {
        const val DEFAULT_VIEW_TYPE = 0
    }

    private var onParentClickListener: OnParentClickListener? = null
    private var onChildClickListener: OnChildClickListener? = null

    // 记录不需要显示child list的列表
    private val hideChildListParentPositionList = ArrayList<Int>()

    final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
    }

    // 创建parent的ViewHolder
    protected abstract fun onCreateParentViewHolder(parent: ViewGroup, viewType: Int): PARENT_VH

    // 创建child的ViewHolder
    protected abstract fun onCreateChildViewHolder(parent: ViewGroup, viewType: Int): CHILD_VH

    final override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
    }

    // 当在onBindViewHolder获取到的view type是parent view type的时候调用
    protected abstract fun onBindParentViewHolder(viewHolder: PARENT_VH, parentPosition: Int, isDisplayedChildList: Boolean)

    // 当在onBindViewHolder获取到的view type是child view type的时候调用
    protected abstract fun onBindChildViewHolder(
        viewHolder: CHILD_VH,
        parentPosition: Int,
        childPosition: Int
    )

    final override fun getItemCount(): Int {
    }

    // 获取parent count
    protected abstract fun getParentCount(): Int

    // 根据parent position获取child count
    protected abstract fun getChildCountFromParent(parentPosition: Int): Int

    final override fun getItemViewType(position: Int): Int {
    }

    // 生成parent view type,这里会调用getParentViewType,子类可以根据需要去实现
    private fun obtainParentViewType(parentPosition: Int): Int {

    }

    protected open fun getParentViewType(parentPosition: Int) = DEFAULT_VIEW_TYPE

    // 生成child view type,这里会调用getChildViewType,子类可以根据需要去实现
    private fun obtainChildViewType(parentPosition: Int, childPosition: Int): Int {

    }

    protected open fun getChildViewType(parentPosition: Int, childPosition: Int) = DEFAULT_VIEW_TYPE

    // 根据parent position判断child list是否显示
    protected fun isDisplayedChildList(parentPosition: Int) =
        hideChildListParentPositionList.contains(parentPosition).not()

    fun setOnParentClickListener(onParentClickListener: OnParentClickListener) {
        this.onParentClickListener = onParentClickListener
    }

    fun setOnChildClickListener(onChildClickListener: OnChildClickListener){
        this.onChildClickListener = onChildClickListener
    }

    abstract class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
}

getItemViewType的实现

上面的getParentViewTypegetChildViewType都是返回0,现在不要想为什么可以这样,接下来会优先实现这两个方法。因为这两个方法是比较重要的,如果这两个方法没有实现,很多方法的细节都不太好写。

getItemViewType:先实现如何判断是parent view type还是child view type。只有先把这个实现了,才能进一步实现自定义parent view type和child view type。

companion object {
    const val PARENT_VIEW_TYPE = 0
    const val CHILD_VIEW_TYPE = 10000
}

final override fun getItemViewType(position: Int): Int {
    var positionCounter = -1
    for (parentPosition in 0 until getParentCount()) {
        positionCounter++
        // 如果拿到的position与positionCounter相等,则是一个parent view type
        if (position == positionCounter) {
            return obtainParentViewType(parentPosition)
        }
        // 如果不是,并且child list display,则看看是不是一个child view type
        if (isChildListDisplay(parentPosition)) {
            val childCount = getChildCountFromParent(parentPosition)
            // 如果position小于等于counter+child count,则说明这个view type是一个child view type,直接计算出child position
            if (position <= positionCounter + childCount ) {
                // 这里需要-1是因为count不可能是一个为0的数字,所以需要-1才能得到正确的position
                return obtainChildViewType(parentPosition, position - positionCounter - 1)
            }
            positionCounter += childCount
        }
    }
    throw IllegalArgumentException("unknow view type for this position:$position")
}
 
// 这里先简单粗暴地用0和10000分别代表parent view type和child view type,我的第一个版本还真就是这样实现的
// 后面肯定会优化代码的,否则我也不可能把代码写成博客
private fun obtainParentViewType(parentPosition: Int): Int = PARENT_VIEW_TYPE
private fun obtainChildViewType(parentPosition: Int, childPosition: Int): Int = CHILD_VIEW_TYPE

在讲如何用比较优雅的方式实现view type之前,先复习一下java的位运算。

  • "|“运算符:当两个数字用”|"运算的时候,bit的处理方式是:只要有一个是1,就得到1。如:111和001两个二进制数用"1"计算出来的结果就是:111。
  • "&“运算符:当两个数字用”&"运算的时候,bit的处理方式是:只要有一个是0,就得到0。如:110和001两个二进制数用"1"计算出来的结果就是:000。

所以我的处理方式是:用int的两个最高位分别代表parent view type和child view type。
所以

// 10000000 00000000 00000000 00000000
const val PARENT_VIEW_TYPE = 0x80000000.toInt()
// 01000000 00000000 00000000 00000000
const val CHILD_VIEW_TYPE = 0x40000000

所以如果想要将一个view type转换成一个parent view type,就使用PARENT_VIEW_TYPE和该view type做"|“运算。想要转换成child view type,就做”&"运算。不过由于使用了这种方式,所以需要验证得到的view type,这个比较简单,下面再提。
方案想到了,但必须要验证自己的方案是否可行,否则当拿去用的时候才发现方案有问题就麻烦了,所以先写一些java代码进行验证。

public class ViewTypeTest {
    @Test
    public void main() {
        testParentViewType();
        testChildViewType();
    }

    private static void testParentViewType() {
        int PARENT_VIEW_TYPE = 0x80000000;

        int viewType = 1;
        int parentViewType = viewType | PARENT_VIEW_TYPE;
        // 到了这里,android studio已经告诉我是true了
        System.out.println((parentViewType & PARENT_VIEW_TYPE) == PARENT_VIEW_TYPE);
        // 使用左移和右移得到原始的view type
        System.out.println((parentViewType << 1 >> 1) == viewType);
    }

    private static void testChildViewType() {
        int CHILD_VIEW_TYPE = 0x40000000;

        int viewType = 1;
        int childViewType = viewType | CHILD_VIEW_TYPE;
        System.out.println((childViewType & CHILD_VIEW_TYPE) == CHILD_VIEW_TYPE);
        System.out.println((childViewType << 2 >> 2) == viewType);
    }
}

true
true
true
true

既然思路没问题,那就把obtainParentViewType和obtainChildViewType方法完善一下。

companion object {
    // 10000000 00000000 00000000 00000000
    private const val PARENT_VIEW_TYPE = 0x80000000.toInt()
    // 01000000 00000000 00000000 00000000
    private const val CHILD_VIEW_TYPE = 0x40000000
    // 取值范围为:[0,CHILD_VIEW_TYPE-1]
    // 00111111 11111111 11111111 11111111
    private const val MAX_VIEW_TYPE = 0x3fffffff
    const val DEFAULT_VIEW_TYPE = 0
}

// 生成parent view type,这里会调用getParentViewType,子类可以根据需要去实现
private fun obtainParentViewType(parentPosition: Int): Int {
    val type = getParentViewType(parentPosition)
    checkViewType(type)
    return type or PARENT_VIEW_TYPE
}

/**
 * @see MAX_VIEW_TYPE
 * @see checkViewType
 * @return parent view type,它可以与child view type相同。然而,它不能大于MAX_VIEW_TYPE也不能为一个负数
 */
protected open fun getParentViewType(parentPosition: Int) = DEFAULT_VIEW_TYPE

// 生成child view type,这里会调用getChildViewType,子类可以根据需要去实现
private fun obtainChildViewType(parentPosition: Int, childPosition: Int): Int {
    val type = getChildViewType(parentPosition, childPosition)
    checkViewType(type)
    return type or CHILD_VIEW_TYPE
}
/**
 * @see MAX_VIEW_TYPE
 * @see checkViewType
 * @return child view type,它可以与parent view type相同。然而,它不能大于MAX_VIEW_TYPE也不能为一个负数
 */
protected open fun getChildViewType(parentPosition: Int, childPosition: Int) = DEFAULT_VIEW_TYPE

private fun checkViewType(viewType: Int) {
    if (viewType < 0 || viewType > MAX_VIEW_TYPE) {
        throw java.lang.IllegalArgumentException("view type :$viewType can't less than 0 or greater than 1073741823(0x3fffffff).")
    }
}

然后再加2个判断是否为parent view type和child view type就行了

protected fun isParentViewType(viewType: Int) = (viewType and PARENT_VIEW_TYPE) == PARENT_VIEW_TYPE

protected fun isChildViewType(viewType: Int) = (viewType and CHILD_VIEW_TYPE) == CHILD_VIEW_TYPE

getItemCount的实现

这个比较简单,只需要简单地遍历而已。

final override fun getItemCount(): Int {
    var count = 0
    for (parentPosition in 0 until getParentCount()) {
        count++
        count += if (isChildListDisplay(parentPosition)) {
            getChildCountFromParent(parentPosition)
        } else {
            0
        }
    }
    return count
}

onCreateViewHolder的实现

这个也比较简单,判断一下view type,如果是parent view type,就create一个parent view holder。如果是child view tyep,就create一个child view holder。

final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
    return when {
        isParentViewType(viewType) -> onCreateParentViewHolder(parent, viewType shl 1 shr 1)
        isChildViewType(viewType) -> onCreateChildViewHolder(parent, viewType shl 2 shr 2)
        else -> throw RuntimeException("unknow view type:$viewType")
    }
}

onBindViewHolder的实现

这个需要根据拿到的position计算出实际的postion,再调用onBindParentViewHolder或onBindChildViewHolder方法。
代码看起来还是比较简单的,所以就不写注释了。

final override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
    var positionCounter = -1
    for (parentPosition in 0 until getParentCount()) {
        positionCounter++
        if (position == positionCounter) {
            val isDisplayedChildList = isDisplayedChildList(parentPosition)
            onBindParentViewHolder(holder as PARENT_VH, parentPosition, isDisplayedChildList)
            holder.itemView.setOnClickListener {
                onParentClickListener?.invoke(parentPosition)
            }
            return
        }
        if (isDisplayedChildList(parentPosition)) {
            val childCount = getChildCountFromParent(parentPosition)
            if (position <= positionCounter + childCount) {
                val childPosition = position - positionCounter - 1
                onBindChildViewHolder(holder as CHILD_VH, parentPosition, childPosition)
                holder.itemView.setOnClickListener {
                    onChildClickListener?.invoke(parentPosition, childPosition)
                }
                return
            } else {
                positionCounter += childCount
            }
        }
    }
}

demo

主要的代码写完了,该出demo了。
这个demo涵盖了对多种parent view type的处理,并且也包含了parent的点击事件,应该把常见的开发场景给还原出来了。
效果图:
在这里插入图片描述
SecondListAdapter.kt

class SecondListAdapter :
    BaseRecyclerExpandableAdapter<SecondListAdapter.ParentViewHolder, SecondListAdapter.ChildViewHolder>() {

    companion object {
        private const val HEADER_1_PARENT_VIEW_TYPE = 1
        private const val HEADER_2_PARENT_VIEW_TYPE = 2
        private const val NORMAL_PARENT_VIEW_TYPE = 3

        private const val HEADER_1_PARENT_POSITION = 0
        private const val HEADER_2_PARENT_POSITION = 1

        private const val HEADER_1_PARENT_VIEW = 1
        private const val HEADER_2_PARENT_VIEW = 1
    }

    val parentList = ArrayList<String>()
    val childMap = HashMap<String, Int>()

    init {
        setOnParentClickListener { parentPosition ->
            if (parentPosition == HEADER_1_PARENT_POSITION || parentPosition == HEADER_2_PARENT_POSITION) {
                return@setOnParentClickListener
            }
            if (isDisplayedChildList(parentPosition)) {
                hideChildList(parentPosition)
            } else {
                displayChildList(parentPosition)
            }
        }
    }

    override fun onCreateParentViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder =
        when (viewType) {
            HEADER_1_PARENT_VIEW_TYPE -> Header1ParentViewHolder(FrameLayout(parent.context))
            HEADER_2_PARENT_VIEW_TYPE -> Header2ParentViewHolder(FrameLayout(parent.context))
            else -> NormalParentViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_second_list, parent, false)
        }

    override fun getParentViewType(parentPosition: Int): Int {
        return when (parentPosition) {
            HEADER_1_PARENT_POSITION -> HEADER_1_PARENT_VIEW_TYPE
            HEADER_2_PARENT_POSITION -> HEADER_2_PARENT_VIEW_TYPE
            else -> NORMAL_PARENT_VIEW_TYPE
        }
    }

    override fun onCreateChildViewHolder(parent: ViewGroup, viewType: Int): ChildViewHolder =
        ChildViewHolder(
            FrameLayout(parent.context)
        )

    override fun onBindParentViewHolder(
        viewHolder: ParentViewHolder,
        parentPosition: Int,
        isDisplayedChildList: Boolean
    ) {
        when (getParentViewType(parentPosition)) {
            HEADER_1_PARENT_VIEW_TYPE -> {
                val vh = viewHolder as Header1ParentViewHolder
                vh.textView.text = "header_1"
            }
            HEADER_2_PARENT_VIEW_TYPE -> {
                val vh = viewHolder as Header2ParentViewHolder
                vh.textView.text = "header_2"
            }
            else -> {
                val realPosition = getRealParentPosition(parentPosition)
                val vh = viewHolder as NormalParentViewHolder
                vh.textView.text = parentList[realPosition]
            }
        }
    }

    override fun onBindChildViewHolder(
        viewHolder: ChildViewHolder,
        parentPosition: Int,
        childPosition: Int
    ) {
    }

    override fun getParentCount(): Int =
        HEADER_1_PARENT_VIEW + HEADER_2_PARENT_VIEW + parentList.size

    override fun getChildCountFromParent(parentPosition: Int): Int =
        when (parentPosition) {
            HEADER_1_PARENT_POSITION, HEADER_2_PARENT_POSITION -> 0
            else -> childMap[parentList[getRealParentPosition(parentPosition)]] ?: 0
        }

    private fun getRealParentPosition(parentPosition: Int) =
        parentPosition - HEADER_1_PARENT_VIEW - HEADER_2_PARENT_VIEW

    open class ParentViewHolder(itemView: View) :
        BaseRecyclerExpandableAdapter.BaseViewHolder(itemView)

    class Header1ParentViewHolder(itemView: FrameLayout) :
        ParentViewHolder(itemView) {
        val textView = TextView(itemView.context).also {
            it.setTextColor(0xff000000.toInt())
            it.textSize = 40f
            it.setPadding(10, 10, 10, 10)
        }

        init {
            itemView.layoutParams = RecyclerView.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
            itemView.addView(textView)
        }
    }

    class Header2ParentViewHolder(itemView: FrameLayout) :
        ParentViewHolder(itemView) {
        val textView = TextView(itemView.context).also {
            it.setTextColor(0xffff0000.toInt())
            it.textSize = 60f
            it.setPadding(10, 10, 10, 10)
        }

        init {
            itemView.layoutParams = RecyclerView.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
            itemView.addView(textView)
        }
    }

    class NormalParentViewHolder(itemView: View) :
    ParentViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(R.id.text)
    }

    class ChildViewHolder(itemView: FrameLayout) :
        BaseRecyclerExpandableAdapter.BaseViewHolder(itemView) {
        val imageView = ImageView(itemView.context).also {
            val dp40 = TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                40f,
                itemView.context.resources.displayMetrics
            ).toInt()
            it.layoutParams = FrameLayout.LayoutParams(dp40, dp40)
            it.setPadding(10, 10, 10, 10)
            it.setImageResource(R.mipmap.ic_launcher)
        }

        init {
            itemView.layoutParams = RecyclerView.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
            itemView.addView(imageView)
        }
    }
}

class SecondListActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second_list)
        recycler.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        val adapter = SecondListAdapter()
        val title1 = "title1"
        val title2 = "title2"
        adapter.parentList.addAll(arrayListOf(title1, title2))
        adapter.childMap[title1] = 4
        adapter.childMap[title2] = 10
        recycler.adapter = adapter
    }
}

item_second_list.xml


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/text"
        android:padding="10dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</FrameLayout>

可以直接到github上面把代码下载下来,然后用上面的代码试试看,点击item1/2时,会显示或隐藏child list

上面这些代码中,或许有一个地方觉得有点疑惑。HEADER_1_PARENT_VIEWHEADER_2_PARENT_VIEW的值都是1,这些写有什么意义?
这算是我在开发的时候想出来的一个小技巧,可以看到他们都被用到了这两个方法。

override fun getParentCount(): Int =
        HEADER_1_PARENT_VIEW + HEADER_2_PARENT_VIEW + parentList.size
        
private fun getRealParentPosition(parentPosition: Int) =
        parentPosition - HEADER_1_PARENT_VIEW - HEADER_2_PARENT_VIEW

这种场景一般有多种处理方式,如:直接写2或者写-1-1,但我发现这种写法其实不存在任何的可读性。如果代码这样写,还需要补注释才能让其他人看懂这种代码。所以我就想到了这种方式,给1起一个大家都看得懂的名字,这样就可以在不编写注释的情况下提升代码的可读性。

优化

上面这些代码是完成了需求,但性能上其实有不少问题。RecyclerView每次调用getItemCount都需要计算一次count,每次调用getItemViewType和onBindViewHolder也都需要重新计算。虽然实际运行的时候,看不出任何卡顿。但这种比较明显的性能问题,还是有必要进行优化。

注册监听方法

// 首先,调用:registerAdapterDataObserver方法,重写所有方法,并提供一个计算方法,让这些被重写的方法都调用这个计算方法。。
init {
        registerDataObserver()
    }

    private fun registerDataObserver() {
        registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
            override fun onChanged() {
                calculateNecessaryData()
            }

            override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
                calculateNecessaryData()
            }

            override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
                calculateNecessaryData()
            }

            override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
                calculateNecessaryData()
            }

            override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
                calculateNecessaryData()
            }

            override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
                calculateNecessaryData()
            }
        })
    }

    private fun calculateNecessaryData(){
    }

calculateNecessaryData方法的实现

companion object {
    const val NO_POSITION = -1
}
// 记录view type
private val viewTypeRecorder = ArrayList<Int>()
// 记录每个position实际的parent position和child position
private val realPositionRecorder = ArrayList<RealPosition>()

protected class RealPosition(val parentPosition: Int, val childPosition: Int)

// 代码非常简单,就不写注释了,这样处理之后,getItemCount和onBindViewHolder的实现就很简单了
private fun calculateNecessaryData() {
    viewTypeRecorder.clear()
    realPositionRecorder.clear()
    for (parentPosition in 0 until getParentCount()) {
        viewTypeRecorder.add(obtainParentViewType(parentPosition))
        realPositionRecorder.add(RealPosition(parentPosition, NO_POSITION))
        if (isDisplayedChildList(parentPosition)) {
            for (childPosition in 0 until getChildCountFromParent(parentPosition)) {
                viewTypeRecorder.add(obtainChildViewType(parentPosition, childPosition))
                realPositionRecorder.add(RealPosition(parentPosition, childPosition))
            }
        }
    }
}

final override fun getItemCount(): Int = viewTypeRecorder.size

final override fun getItemViewType(position: Int): Int = viewTypeRecorder[position]

final override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
    val realPosition = realPositionRecorder[position]
    if (realPosition.childPosition == NO_POSITION) {
        val isDisplayedChildList = isDisplayedChildList(realPosition.parentPosition)
        onBindParentViewHolder(
            holder as PARENT_VH,
            realPosition.parentPosition,
            isDisplayedChildList
        )
        holder.itemView.setOnClickListener {
            onParentClickListener?.invoke(realPosition.parentPosition)
        }
        return
    }
    onBindChildViewHolder(
        holder as CHILD_VH,
        realPosition.parentPosition,
        realPosition.childPosition
    )
    holder.itemView.setOnClickListener {
        onChildClickListener?.invoke(realPosition.parentPosition, realPosition.childPosition)
    }
}

protected fun getRealPosition(position: Int): RealPosition? =
    realPositionRecorder.getOrNull(position)

最后,需要注意的是,在将adapter设置到RecyclerView之后需要手动调用一次notify方法。因为直接设置的话并不会触发register里面的方法,此时,getItemCouunt等方法就获取不到数据,所以需要手动调用一次。
用了这种方式优化之后,每次滑动就直接从缓存中去数据,而不用重新计算。所以效率提升了不少,而且逻辑也更清晰了。第一个版本的代码,计算item count、view type等方法看起来还是比较复杂的,但用了这种方式就变得特别直观了。

二级列表的悬浮功能

这种功能百度可以找出一堆,但当我在开发的时候,发现百度找到的那些代码都不能解决我的问题。无奈只能自己想办法,刚好那个时候手头上已经有了这个adapter,所以借鉴了百度找到的代码自己实现。

先说一下为什么需要自己实现吧。是这样的,有一个列表界面,这个列表头上有几个Header。所以一开始这个界面的做法就是外部一个NestedScrollView,然后几个header view+RecyclerView,由于NestedScrollView可以将RecyclerVie的高度变得很长,所以简单粗暴地解决了问题。后面说要加悬浮的功能,将百度找到的ItemDecoration套进去,发现不行。因为这个时候发现recylerView的getChildAt(0)获取到的永远是最上面的view(header下面的view),而不是可见的第一个。这个时候才意识到用NestedScrollView会出现性能问题。然后才用view type加了几个header来解决。
具体看代码吧,如果有类似的需求,用这种方式解决比较好。而且我在百度找到的很多代码,是没办法实现点击事件的。因为那些界面都是绘制出来,不是一个实体,没办法添加。但用BaseRecyclerExpandableAdapter去实现的话,就可以添加点击事件。

效果图就看博客开头的图片,那就是完整的实现方式

// FloatItemDecoration.kt
class FloatItemDecoration(private val recyclerView: RecyclerView) : RecyclerView.ItemDecoration() {
    interface StickHeaderInterface {
        fun isStick(position: Int): Boolean
    }

    private val linearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
    private val adapter =
        recyclerView.adapter ?: throw RuntimeException("please set Decoration after set adapter")
    private val stickHeaderInterface = adapter.let {
        if (it !is StickHeaderInterface) {
            throw RuntimeException("please let your adapter implements StickHeaderInterface")
        }
        adapter as StickHeaderInterface
    }

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

        // 获取第一个可见的view和他的position
        val firstChild = parent.getChildAt(0) ?: return
        val position = parent.getChildAdapterPosition(firstChild)
        // 向该view的上面寻找,目的是找出可以stick的view
        for (i in position downTo 0) {
            // 如果找到了
            if (stickHeaderInterface.isStick(i)) {
                var top = 0
                // 这里两个if的作用是:当position的下一个view,即屏幕可见的那个view的下一个view需要stick
                // 的时候,就获取该view的top,并且当该view的top大于的时候,top的值用该view的top
                // 这里的代码很关键,正因为有了这两个if里面的代码,才实现了两个stick view贴在一起
                // 一起向上或向下滚动的效果
                if (position + 1 < adapter.itemCount) {
                    if (stickHeaderInterface.isStick(position + 1)) {
                        val nextChild = parent.getChildAt(1)
                        top = Math.max(linearLayoutManager.getDecoratedTop(nextChild), 0)
                    }
                }
                val holder = adapter.createViewHolder(parent, adapter.getItemViewType(i))
                adapter.bindViewHolder(holder, i)
                // 注意:这里计算的是在i的位置的view的大小,不是postion的位置
                val measureHeight = getMeasureHeight(holder.itemView)
                c.save()
                // 只有当top小于第一个view的高度的时候,并且top大于0,画布才向上滚动
                if (top < measureHeight && top > 0) {
                    c.translate(0f, ((top - measureHeight).toFloat()))
                }
                holder.itemView.draw(c)
                return
            }
        }
    }

    private fun getMeasureHeight(header: View): Int {
        val widthSpec =
            View.MeasureSpec.makeMeasureSpec(recyclerView.width, View.MeasureSpec.EXACTLY)
        val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
        header.measure(widthSpec, heightSpec)
        header.layout(0, 0, header.measuredWidth, header.minimumHeight)
        return header.measuredHeight
    }
}

// SecondListAdapter.kt
// 实现FloatItemDecoration.StickHeaderInterface接口并且加这样一段代码
override fun isStick(position: Int): Boolean {
    // 如果该position是一个parent view,并且不是header,就返回true,因为header不能stick
    return getRealPosition(position)?.let { realPosition ->
        if (realPosition.childPosition != NO_POSITION) {
            return false
        }
        realPosition.parentPosition != HEADER_1_PARENT_POSITION && realPosition.parentPosition != HEADER_2_PARENT_POSITION
    } ?: false
}

如果需要点击事件,就在adapter里面自己加吧,这里就不赘述了。

关于抛出异常的代码
看了上面的代码,可以发现,不少代码执行到else时,就会抛出异常。这种做法可能会导致APP运行时崩溃,如果担心出现问题,可以把抛异常的代码改成log.e或log.wtf这种代码。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值