RecyclerView嵌套RecyclerView的加载更多问题

Posted 汤米粥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RecyclerView嵌套RecyclerView的加载更多问题相关的知识,希望对你有一定的参考价值。

recyclerview嵌套,子层recyclerview加载更多,会导致一直加载的问题,于是指定子层recyclerView的高度,但指定了高度子层的recyclerview就不能滑动,下面这篇文字能了解决这个问题。

https://blog.csdn.net/ganduwei/article/details/125025149

-----------------------------------------------------------------------------------------------------------------------

一、概述
虽然今天我们要说的是Rv嵌套Rv的问题,但多数情况下我们都不会使用Rv嵌套Rv,来实现复杂的列表,而是使用多ItemType实现,可能再复杂点的,配合GridLayoutManager.SpanSizeLookup一起来实现,再高级点的自定义LayoutManager实现。
Rv嵌套Rv会有问题,如果嵌套的Rv高度没有设置明确的值,会一次创建所有的item,造成卡顿。类似我们在NestedScrollView里面嵌套Rv,Rv的高度写的是wrap_content或match_parent,一样的情况。
既然高度不确定,那我们给嵌套的Rv指定高度,不就不会一次创建所有item了吗,可真要是这么做,你就会发现嵌套的Rv无法滑动,只能滑动外部的父Rv。
疑惑为啥在NestedScrollView里面嵌套的Rv指定高度后,Rv是能正常滑动的呢?不用奇怪,NestedScrollView之所以叫这个名字,是因为他本身是支持嵌套滑动的。

我们一般不会使用Rv嵌套Rv,但并不是我们不用就不会出现。
有时你可能遇到一个很老的代码,他就是这么实现的,并且还出现了卡顿问题,需要优化。如果你完全改变实现方式使用多ItemType,那改动肯定会很大。在时间不充裕且不能出新bug的情况下,限制子Rv的高度,应该是最好的办法,只要解决子Rv滑动问题。
或者有时,UI出的某个页面,就必须通过Rv嵌套Rv实现,就像这样:

那我们有办法让子Rv正常滑动吗?办法肯定有:

一种是像NestedScrollView,通过嵌套滑动机制;
另一种是基于传统的事件分发机制,请求父Rv不要拦截事件;
下面我们通过第2种方式实现。

二、实现思路
很明显事件被父Rv全部拦截了,所以子Rv不能滑动。我们的思路是,监听事件,如果手指触摸的是子Rv,并且子Rv能滑动,就告诉父Rv不要拦截事件,由子Rv处理。
思路有了,有几点需要考虑如何实现:

如何监听事件?很容易,通过TouchListener即可。
在哪里监听?直接给子Rv设置OnTouchListener 还是 给父Rv 添加 OnItemTouchListener?可能两个地方都可以,需要去试。我已经试过了,答案是给父Rv 添加 OnItemTouchListener。给子Rv设置setOnTouchListener,似乎可以。但实现后发现子Rv时而可以滑动,时而不可以滑动,可能父Rv优先收到事件,还是会直接拦截事件,压根走不到子Rv的onTouch里面。通过给父Rv设置OnItemTouchListener 能保证item始终能收到点击事件,OnItemTouchListener 对事件的处理优先于父Rv。
如何获取手指触摸位置的子Rv?通过父Rv.findChildViewUnder(x, y) 可以拿到触摸位置的 view,再通过父Rv.getChildViewHolder(view)拿到viewHolder,拿到viewHolder便拿到子Rv了。
如何判断子Rv能不能滑动?通过子Rv.canScrollVertically(1) 方法判断能否向上滑动,返回true能; 通过子Rv.canScrollVertically(-1) 方法判断能否向下滑动,返回true能;
如何告诉父Rv不要拦截事件?通过子Rv.requestDisallowInterceptTouchEvent(true)。
三、核心代码
上面思路是我们的核心,其他的都是类似套路,Adapter,点击事件等等。下面是核心代码,相关注释很明确。

rv.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() 
    var viewHolder: ParentViewHolder? = null
    var mY = 0f
    override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean 
        when (e.action) 
            MotionEvent.ACTION_DOWN -> 
                rv.findChildViewUnder(e.x, e.y)?.let //找到手指点击的itemView
                    val vh = rv.getChildViewHolder(it)//获取点击的viewHolder
                    (vh as? ParentViewHolder)?.let  parentVh ->
                        val childRv = parentVh.childRv
                        val isVisible = childRv.visibility == View.VISIBLE//是否可见
                        val canUpScroll = childRv.canScrollVertically(1)//能否向上滑动
                        val canDownScroll = childRv.canScrollVertically(-1)//能否向下滑动
                        if (isVisible && (canUpScroll || canDownScroll)) //可见,并且能滑动,请求父Rv不拦截事件
                            viewHolder?.childRv?.requestDisallowInterceptTouchEvent(true)
                            viewHolder = vh
                            mY = e.y
                        
                    
                
            
            MotionEvent.ACTION_MOVE -> 
                val childRv = viewHolder?.childRv ?: return false//item里面的Rv
                val diff = mY - e.y
                mY = e.y
                if (diff >= 0) //手指向上滑动
                    //RecyclerView.canScrollVertically(1)的值表示是否能向上滚动,false表示已经滚动到底部
                    if (childRv.canScrollVertically(1)) //子Rv未滑到底部,请求父Rv不拦截事件
                        childRv.requestDisallowInterceptTouchEvent(true)
                     else //子Rv滑到底部了,父Rv可以拦截事件
                        childRv.requestDisallowInterceptTouchEvent(false)
                    
                 else //手指向下滑动
                    //RecyclerView.canScrollVertically(-1)的值表示是否能向下滚动,false表示已经滚动到顶部
                    if (childRv.canScrollVertically(-1)) //子Rv未滑到顶部,请求父Rv不拦截事件
                        childRv.requestDisallowInterceptTouchEvent(true)
                     else //子Rv滑到顶部了,父Rv可以拦截事件
                        childRv.requestDisallowInterceptTouchEvent(false)
                    
                
            
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> 
                viewHolder?.childRv?.requestDisallowInterceptTouchEvent(false)
                mY = 0f
                viewHolder = null
            
        
        return false
    
)
 
四、完整代码
RvNestedRvActivity
class RvNestedRvActivity : AppCompatActivity(), IActionListener 
    val adapter = ParentAdapter(this)
    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_nested)
        supportActionBar?.title = "RvNestedRv"
        val rv = findViewById<RecyclerView>(R.id.recyclerView)
        rv.layoutManager = LinearLayoutManager(this)
        rv.adapter = adapter
        rv.setHasFixedSize(true)

        val list = ArrayList<ParentBean>()
        for (i in 0..90) 
            val parentBean = ParentBean()
            parentBean.name = "Parent $i"
            val childList = ArrayList<ChildBean>()
            for (jj in 0..50) 
                val childBean = ChildBean()
                childBean.name = "Child i$i-$jj"
                childList.add(childBean)
            
            parentBean.childList = childList
            list.add(parentBean)
        
        adapter.list.addAll(list)
        rv.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() 
            var viewHolder: ParentViewHolder? = null
            var mY = 0f
            override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean 
                when (e.action) 
                    MotionEvent.ACTION_DOWN -> 
                        rv.findChildViewUnder(e.x, e.y)?.let //找到手指点击的itemView
                            val vh = rv.getChildViewHolder(it)//获取点击的viewHolder
                            (vh as? ParentViewHolder)?.let  parentVh ->
                                val childRv = parentVh.childRv
                                val isVisible = childRv.visibility == View.VISIBLE//是否可见
                                val canUpScroll = childRv.canScrollVertically(1)//能否向上滑动
                                val canDownScroll = childRv.canScrollVertically(-1)//能否向下滑动
                                if (isVisible && (canUpScroll || canDownScroll)) //可见,并且能滑动,请求父Rv不拦截事件
                                    viewHolder?.childRv?.requestDisallowInterceptTouchEvent(true)
                                    viewHolder = vh
                                    mY = e.y
                                
                            
                        
                    
                    MotionEvent.ACTION_MOVE -> 
                        val childRv = viewHolder?.childRv ?: return false//item里面的Rv
                        val diff = mY - e.y
                        mY = e.y
                        if (diff >= 0) //手指向上滑动
                            //RecyclerView.canScrollVertically(1)的值表示是否能向上滚动,false表示已经滚动到底部
                            if (childRv.canScrollVertically(1)) //子Rv未滑到底部,请求父Rv不拦截事件
                                childRv.requestDisallowInterceptTouchEvent(true)
                             else //子Rv滑到底部了,父Rv可以拦截事件
                                childRv.requestDisallowInterceptTouchEvent(false)
                            
                         else //手指向下滑动
                            //RecyclerView.canScrollVertically(-1)的值表示是否能向下滚动,false表示已经滚动到顶部
                            if (childRv.canScrollVertically(-1)) //子Rv未滑到顶部,请求父Rv不拦截事件
                                childRv.requestDisallowInterceptTouchEvent(true)
                             else //子Rv滑到顶部了,父Rv可以拦截事件
                                childRv.requestDisallowInterceptTouchEvent(false)
                            
                        
                    
                    MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> 
                        viewHolder?.childRv?.requestDisallowInterceptTouchEvent(false)
                        mY = 0f
                        viewHolder = null
                    
                
                return false
            
        )
    

    override fun onParentClick(position: Int, bean: ParentBean) 
        //展开状态,点击折叠;折叠状态,点击展开
        bean.isExpand = !bean.isExpand
        //刷新item
        adapter.notifyItemChanged(position)
    

    override fun onChildClick(position: Int, bean: ChildBean) 
        Toast.makeText(this, bean.name, Toast.LENGTH_SHORT).show()
    

 
 
IActionListener
interface IActionListener 
    fun onParentClick(position: Int, bean: ParentBean)
    fun onChildClick(position: Int, bean: ChildBean)

 
ParentAdapter
class ParentAdapter(private val listener: IActionListener?) : RecyclerView.Adapter<ParentViewHolder>() 
    val list = ArrayList<ParentBean>()
    //子Rv用的缓存池
    private val recyclerPool = RecyclerView.RecycledViewPool()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder 
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.nested_rv_parent_item, parent, false)
        return ParentViewHolder(view, listener)
    

    override fun onBindViewHolder(holder: ParentViewHolder, position: Int) 
        holder.onBind(list[position])
        //让子Rv共用同一个缓存池
        holder.childRv.setRecycledViewPool(recyclerPool)
    

    override fun getItemCount() = list.size


 
ParentBean
class ParentBean 
    var childList: ArrayList<ChildBean>? = null
    var name: String? = null
    var isExpand = false

 
ParentViewHolder
class ParentViewHolder(view: View, private val listener: IActionListener?) :
    RecyclerView.ViewHolder(view), View.OnClickListener 
    val childRv: RecyclerView = view.findViewById<RecyclerView>(R.id.childRv).apply 
        setHasFixedSize(true)
        layoutManager = GridLayoutManager(context, 4).apply 
            spanSizeLookup
        
    
    private val textTv = view.findViewById<TextView>(R.id.text)
    private val imageIv = view.findViewById<ImageView>(R.id.image)

    fun onBind(bean: ParentBean) 
        textTv.text = bean.name
        imageIv.rotation = if (bean.isExpand) 180f else 0f
        val childList = bean.childList
        if (bean.isExpand && childList != null) //展开状态,显示子Rv
            childRv.visibility = View.VISIBLE
            var adapter = childRv.adapter
            if (adapter is ChildAdapter) 
                adapter.list.clear()
                adapter.list.addAll(childList)
                adapter.notifyDataSetChanged()
             else 
                adapter = ChildAdapter(listener).apply 
                    list.addAll(childList)
                
                childRv.adapter = adapter
            
         else //折叠状态,隐藏子Rv
            childRv.visibility = View.GONE
        
        textTv.setOnClickListener(this)
        itemView.tag = bean
    

    override fun onClick(v: View?) 
        val bean = itemView.tag as? ParentBean ?: return
        when (v?.id) 
            R.id.text -> 
                listener?.onParentClick(adapterPosition, bean)
            
        
    


 
ChildAdapter
class ChildAdapter(private val listener: IActionListener?) : RecyclerView.Adapter<ChildViewHolder>() 

    val list = ArrayList<ChildBean>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChildViewHolder 
        Log.i("TAG", "ChildAdapter onCreateViewHolder viewType $viewType")
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.nested_rv_child_item, parent, false)
        return ChildViewHolder(view,listener)
    

    override fun onBindViewHolder(holder: ChildViewHolder, position: Int) 
        Log.i("TAG", "ChildAdapter onBindViewHolder position $position")
        holder.onBind(list[position])
    

    override fun getItemCount(): Int 
        return list.size
    


 
ChildBean
class ChildBean 
    var name: String? = null

 
ChildViewHolder
class ChildViewHolder(view: View, private val listener: IActionListener?) :
    RecyclerView.ViewHolder(view),
    View.OnClickListener 
    private val textTv = view.findViewById<TextView>(R.id.textView)
    fun onBind(bean: ChildBean) 
        textTv.text = bean.name
        textTv.setOnClickListener(this)
        itemView.tag = bean
    

    override fun onClick(v: View?) 
        val bean = itemView.tag as? ChildBean ?: return
        when (v?.id) 
            R.id.textView -> 
                listener?.onChildClick(adapterPosition, bean)
            
        
    

 
五、不足
不能实现子Rv滑动完之后,父Rv接着滑动
某些情况下(快速滑动),父Rv还是会优先滑动,尽管触摸的是子Rv

这些不足可能是传统的事件分发机制无法解决的,要避免这些问题,需要使用嵌套滑动机制实现,后面我会基于这种方式实现。
 

以上是关于RecyclerView嵌套RecyclerView的加载更多问题的主要内容,如果未能解决你的问题,请参考以下文章

RecyclerView

RecyclerView的那点事儿

如何让 RecyclerView 更新其布局?

Android-Recyclerview-使用分割线

在RecyclerView中遇到onClickListener问题[重复]

RecyclerView系列:RecyclerView基本使用java版