一、概述
虽然今天我们要说的是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 {
val vh = rv.getChildViewHolder(it)
(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)) {
viewHolder?.childRv?.requestDisallowInterceptTouchEvent(true)
viewHolder = vh
mY = e.y
}
}
}
}
MotionEvent.ACTION_MOVE -> {
val childRv = viewHolder?.childRv ?: return false
val diff = mY - e.y
mY = e.y
if (diff >= 0) {
if (childRv.canScrollVertically(1)) {
childRv.requestDisallowInterceptTouchEvent(true)
} else {
childRv.requestDisallowInterceptTouchEvent(false)
}
} else {
if (childRv.canScrollVertically(-1)) {
childRv.requestDisallowInterceptTouchEvent(true)
} else {
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 {
val vh = rv.getChildViewHolder(it)
(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)) {
viewHolder?.childRv?.requestDisallowInterceptTouchEvent(true)
viewHolder = vh
mY = e.y
}
}
}
}
MotionEvent.ACTION_MOVE -> {
val childRv = viewHolder?.childRv ?: return false
val diff = mY - e.y
mY = e.y
if (diff >= 0) {
if (childRv.canScrollVertically(1)) {
childRv.requestDisallowInterceptTouchEvent(true)
} else {
childRv.requestDisallowInterceptTouchEvent(false)
}
} else {
if (childRv.canScrollVertically(-1)) {
childRv.requestDisallowInterceptTouchEvent(true)
} else {
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
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>()
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])
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) {
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 {
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
- …
这些不足可能是传统的事件分发机制无法解决的,要避免这些问题,需要使用嵌套滑动机制实现,后面我会基于这种方式实现。
|