RecyclerView嵌套RecyclerView的滑动问题如何解

一、概述

虽然今天我们要说的是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

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

猜你喜欢

转载自blog.csdn.net/ganduwei/article/details/125025149