实现RecyclerView二级列表

使用RecyclerView+ViewDataBinding实现简单的二级列表 前言 效果预览 RecyclerView与Adapter交互流程 a. 调用Adapter.getItemCount()方法,RecyclerView询问数组列表中包含多少个待展示的视图。  b. RecyclerView调用Adapter.onCreateViewHolder(ViewGroup, int viewType)创建ViewHolder。其中viewType是方法getItemViewType(int position)给它的,二级列表需要关注viewType,因为明显一级视图与二级 阅读详情

自定义RecyclerView的adapter实现二级列表
图片大于5MB,优快云不让上传,使用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的实现

上面的getParentViewType和getChildViewType都是返回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_VIEW和HEADER_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这种代码。

Android实现RecyclerView二级列表 (附完整源码) Android实现RecyclerView二级列表 (附完整源码) 阅读详情

相关推荐

Android RecyclerView 二级列表实现.zip.zip

Android RecyclerView 二级列表实现,可自定义item的样式,自定义折叠效果,自定义展开方式

RecyclerView二级列表

最近正好有做到二级列表,就记载一下怎样使用RecyclerView做二级列表吧。 效果大概就是这个样子,可以凑合用,主要是弄清楚大概原理,这样就知道步骤。代码 ...

明光的博客 6616

Android RecyclerView 二级列表(包含父列表和子列表)json格式是集合包含集合形式

Android RecyclerView 二级列表(包含父列表和子列表)json格式是集合包含集合形式,实现了item点击监听事件。

RecycleView 二级列表(多级列表)

RecyclerView做二级列表 理论上也可以做N级列表去套娃即可

qq_46267953的博客 2758

使用RecycleView实现动态获取数据展开二级列表展示

使用RecycleView实现动态获取数据展开二级列表展示 - **可展开列表的RecycleView** - **二级列表数据动态获取** - **优化原文中获取原始一级列表位置方式**

Android使用RecyclerView实现二级联动,并且选中后保持高亮状态

使用RecyclerView实现二级联动功能,代码简洁明了的实现选中一级列表后会保持高亮,点击二级列表会有相应提示

Android RecyclerView 二级列表实现

Android RecyclerView 二级列表实现 简述 实现基础 实现思路 实现过程 二级列表数据格式 RecyclerView Item状态 ViewHolder 其它属性和方法 getItemStatusByPosition 方法实现 getItemCount方法实现 其它方法实现 总结 附Android RecyclerView 二级列表实现简述在开发 Android APP 的时候,难

yuhys的博客 2万+

手把手教你用ComfyUI搭建高效AI绘画工作流:从基础到高级技巧

本文详细介绍了如何使用ComfyUI构建高效的AI绘画工作流,从基础配置到高级技巧全面覆盖。通过模块化设计,ComfyUI显著提升显存利用率和创作灵活性,适合从新手到专业用户。文章包含部署指南、工作流搭建、性能优化及行业应用案例,帮助读者快速掌握AI绘画核心技术。

7up55的博客 720

Android实现RecyclerView二级列表(附带源码)

Android实现RecyclerView二级列表(附带源码)

欢迎来到我的博客!这里是一个专注于计算机技术的分享平台,涵盖编程开发、算法研究、系统架构、软件工程等多个领域。无论你是初学者还是资深开发者,都能在这里找到有价值的内容。 1481

从零构建:STM32与EC800 4G模块的工业物联网数据透传实战

本文详细介绍了如何从零构建STM32与EC800 4G模块的工业物联网数据透传系统。内容涵盖硬件选型、抗干扰电路设计、AT指令稳定性优化、网络连接管理、可靠数据透传协议以及低功耗策略,为开发者提供了一套可直接应用于工业场景的完整实战方案。

uran的专栏 1014

在win10-wsl2(ubuntu)中安装jlink

在 官方下载linux版本的deb包拷贝到ubuntu的/home目录下(其他目录也可以),进行安装然后提示很多依赖缺失的错误,此时执行自动安装需要的依赖包再次重新安装就可以啦查看是否安装成功,输入JLink 再按下两次Table键,是否如下所示这样表示安装成功了。

yaq_30401的博客 3140

android 二级列表拖动排序_Android RecyclerView 二级列表实现

[TOC]Android RecyclerView 二级列表实现2017.5.16 添加demogit简述在开发 Android APP 的时候,难免会需要实现二级列表的情况,而在自己的项目中使用的列表是 android.support.v7.widget 包里面的 RecyclerView,好处是可以根据情况实现不同样式的列表,可扩展程度高。而坏处是什么都要自己实现。所以在想要用 Recycle...

weixin_39792686的博客 398

Basilisk vs. STK:开源航天仿真工具在Windows上的实战体验与生态对比

本文对比了开源航天仿真工具Basilisk与商业软件STK在Windows平台上的实战体验与生态差异。从技术架构、安装配置、使用成本、功能覆盖、社区生态到适用场景,全面分析了二者的优劣势,为航天工程师和研究人员提供选型参考。Basilisk在性能和扩展性上表现突出,而STK在可视化与专业模块上更具优势。

weixin_30632089的博客 589

RecyclerView实现二级列表

那今天就教大家使用RecyclerView来实现二级列表的购物车 废话不多说 直接上代码 权限 android:name="android.permission.INTERNET"> android:name="android.permission.READ_EXTERNAL_STORAGE" /> android:name="android.permission.WRIT

cm1606530815的博客 4769

手把手教你用C#实现TwinCAT 3 ADS通讯(附完整代码示例)

本文详细解析了如何使用C#实现TwinCAT 3 ADS通讯,包括协议核心价值、开发环境配置、变量通信实践及高级应用场景。通过完整代码示例和性能优化技巧,帮助开发者高效实现工业自动化系统中的实时数据交换,特别适合倍福(Beckhoff)平台用户。

weixin_29230901的博客 117

FFmpeg 部署与使用(含Docker部署)

ffmpeg 部署以及使用

weixin_38584583的博客 911

android 跨行列表,Android RecyclerView 二级列表实现

[TOC]Android RecyclerView 二级列表实现2017.5.16 添加demogit简述在开发 Android APP 的时候,难免会需要实现二级列表的情况,而在自己的项目中使用的列表是 android.support.v7.widget 包里面的 RecyclerView,好处是可以根据情况实现不同样式的列表,可扩展程度高。而坏处是什么都要自己实现。所以在想要用 Recycle...

weixin_39951018的博客 469

论文必备 - YOLOv12热力图可视化,支持指定模型,指定显示层,设置置信度,以及10种可视化实现方式

本文带来的是YOLOv12热力图可视化功能,支持,以及。我们经常看到一些论文里绘制了不同的热力图,一方面能够直观的感受其模型的有效性,另一方面也丰富了论文内容。特别是在使用了注意力模块的网络中,热力图就可以验证注意力机制是否真正聚焦到了预期的重要特征上,以便对模型的有效性和合理性进行评估。例如这篇文章中展示的,就很能够表达作者改进后的模型相比之前模型的优越性。

Limiiiing的博客 1706

Android实现RecyclerView二级列表可折叠展开选中

前言 项目里需要实现个可折叠,可展开的的二级列表,首先想到了用ExpandListView去实现,ExpandListView是继承ListView的。由于项目里所有列表都用Recycleview,再加上本身对于Recyclerview情有独钟,懂的都懂,就想着试试用它实现吧。 效果图 实现 网上找到了ExpandableRecyclerView这个库,读了一遍源码,然后将BaseExpandableRecyclerViewAdapter拷贝到项目中,简单能够显示出来出来,因为我们的业务需求需要实现可选择

qq_28898075的博客 5223

基于simulink的多能源系统协同与能源互联网的电网系统场景实例:虚拟电厂(VPP)中电网与储能集群的协同控制

本文详细介绍了使用Simulink构建虚拟电厂(VPP)与储能集群协同控制系统的完整建模过程。主要内容包括:1) VPP系统架构设计;2)分步骤建模过程,涵盖虚拟电厂系统创建、储能单元与集群建模、VPP控制器设计(核心)以及协同控制场景仿真;3)性能分析与典型应用场景。通过混合整数线性规划优化调度和智能指令分解策略,实现了VPP参与电力市场交易、电网调频调峰等功能,验证了系统在提升可再生能源消纳率(96%)、降低电网峰谷差(35%)等方面的优异性能。该建模方法为能源互联网中的分布式资源聚合与协同控制提供了实

xiaoheshang_123的博客 471

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Android中RecyclerView点击item展开二级列表的简单实现(代码详细讲解)

Android中RecyclerView可展开二级列表及非展开二级列表(代码详细讲解) 一、大体介绍   最近在学安卓的基础知识recylcerview,然后去网上搜了很多代码,发现写的比较复杂,于是自己在leader的指导下写了一个简单的代码,并且有完整的代码实现以及详细代码讲解。想要学习的小伙伴可不要错过了!所有的完整代码都标注了!   最开始学习此view的时候是由浅入深的,因此如果一开始就上来做可展开二级列表会有点难度,因此我们可以从最简单的非展开二级列表来完成,然后循序渐进,做出来可折叠的二级列.

weixin_43436958的博客 6534

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

android 二级列表 滚动,recyclerview实现二级列表

/*** 自定义recyclerview继承relativeLayout*/public class ExpandableRecyclerView extends RelativeLayout{// 悬浮在顶部的viewprivate RelativeLayout pinnedView;ImageView pinnedIcon; // 顶部悬浮view的icon,可用来表示展开状态TextView...

weixin_32279335的博客 738

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

上一篇: Android accessibility开发笔记
下一篇: android事件分发机制源码分析
杜壁奇
博客等级 码龄10年 10粉丝 31原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值