背景:2个同向滑动的RecyclerView相互嵌套,进行滑动时发生滑动冲突。例如,Viewpager2内部包含了另一个ViewPager2或一个横向滑动的recyclerView。这时,在横向滑动时,滑动的可能是外部的ViewPager2,也可能是内部的RecyclerView。
滑动冲突的原因:外部ViewPager2和内部RecyclerView的滑动阈值不同造成滑动冲突。ViewPager2的滑动阈值较高,内部RecyclerView的滑动阈值较低。具体来说,在触摸事件分发时,ViewPager2会先尝试对事件进行拦截,若滑动的距离大于了ViewPager2的滑动阈值,则ViewPager2会对此事件进行拦截,即给之前处理事件的子视图发一个Cancel通知,然后自己去处理后继的所有事件。比如,对于包含有RecyclerView的ViewPager2来说,由于滑动距离的增长快慢不同,会导致不同的视图处理滑动事件;1、滑动距离增长缓慢,当滑动距离超过了RecyclerView的滑动阈值且低于ViewPager2的滑动阈值时,RecyclerView会处理事件,VewPager2不会处理事件,且RecclerView在发生滑动时,会通知父视图不要拦截之后的事件;2、当滑动距离突增,超过了ViewPager2的滑动阈值时,ViewPager2会处理事件,并对事件进行拦截,这样就造成内部的RecyclerView无法处理事件。这就是滑动冲突产生的原因。
解决思路:当内部的RecyclerView可以滑动时,优先滑动内部的RecyclerView;内部视图不可以滑动时,则滑动外部的ViewPager2。为了实现此目标,针对实际种可能存在的2种不同情况,给出了不同的解决方案。
视图嵌套场景一:外部视图的滑动阈值大,内部视图的滑动阈值小的情况。例如,ViewPager2包含一个RecyclerView
解决方法一:对外部视图的dispatchTouchEvent方法进行重写,在滑动距离首次超过自身的滑动阈值时,先发送一个滑动距离等于自身滑动阈值的事件,以使滑动阈值较小的内部视图可以优先处理事件,然后再发生原触摸事件。对于上述场景例子来说,因为外部的ViewPager2用final进行了修饰,不能被继承,进而不能重写dispatchTouchEvent方法,所以需要一个“壳”View,将ViewPager2放到此“壳”View内,并重写壳view的dispatchTouchEvent。壳View代码可以如下:
/**
* 此视图内部只能包含一个RecyclerView
*/
class OutsideLinearLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
LinearLayout(context, attrs, defStyleAttr) {
//记录手指按下时的坐标
var mInitialTouchX: Float = -1f
var mInitialTouchY: Float = -1f
//判断是否需要在分发原MotionEvent事件之前,分发一个滑动距离为滑动阈值的事件
var splitFlag = false
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, -1)
constructor(context: Context?) : this(context, null)
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
ev?.let {
if(it.action == MotionEvent.ACTION_DOWN){
mInitialTouchX = it.x
mInitialTouchY = it.y
splitFlag = true
}else if(it.action == MotionEvent.ACTION_MOVE){
var recyclerView = searchRecyclerView(this)
//获取ViewPager2的滑动阈值
var touchSlop = getRecyclerViewTouchSlop(recyclerView!!)
var isScroll = Math.abs(it.x - mInitialTouchX)>touchSlop||Math.abs(it.y - mInitialTouchY)>touchSlop
//检查是否需要插入一个滑动距离为滑动阈值的触摸事件
//需要满足的条件为:1、滑动距离里大于滑动阈值;2、自Down事件发生后,第一次大于滑动阈值
if(isScroll&&splitFlag){
var newMotionEvent = MotionEvent.obtain(it)
var newX = it.x
if(Math.abs(it.x - mInitialTouchX)>touchSlop){
//使新MotionEvent的x与手指按下时的x之间的差为滑动阈值
if(it.x-mInitialTouchX>0){
newX = mInitialTouchX+touchSlop
}else newX = mInitialTouchX-touchSlop
}
var newY = it.y
if(Math.abs(it.y - mInitialTouchY)>touchSlop){
if(it.y-mInitialTouchY>0){
newY = mInitialTouchY+touchSlop
}else newY = mInitialTouchY-touchSlop
}
newMotionEvent.setLocation(newX, newY)
//发生新的MotionEvent
super.dispatchTouchEvent(newMotionEvent)
splitFlag = false
}
}
}
return super.dispatchTouchEvent(ev)
}
/**
* 获取RecyclerView的滑动阈值
* @param recyclerView RecyclerView
* @return Int 滑动阈值
*/
fun getRecyclerViewTouchSlop(recyclerView: RecyclerView): Int {
var clazz = RecyclerView::class.java
var field = clazz.getDeclaredField("mTouchSlop")
field.isAccessible = true
var touchSlop = field.get(recyclerView) as Int
return touchSlop
}
/**
* 获取ViewGroup中的RecyclerView
* @param vg ViewGroup
* @return RecyclerView?
*/
fun searchRecyclerView(vg: ViewGroup):RecyclerView?{
for(i in 0 until vg.childCount){
var view = vg.getChildAt(i)
if(view is RecyclerView){
return view
}else if(view is ViewGroup){
var recyclerView = searchRecyclerView(view)
if(recyclerView!=null){
return recyclerView
}
}
}
return null
}
}
将ViewPager2放到壳View中:
<com.example.icemusic.view.ViewPager2LinearLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/view_pager2_linear_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/tabLayout">
<androidx.viewpager2.widget.ViewPager2
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/viewPager">
</androidx.viewpager2.widget.ViewPager2>
</com.example.icemusic.view.ViewPager2LinearLayout>
视图嵌套场景二:外部视图的滑动阈值和内部视图的滑动阈值相同。例如,外部ViewPager2里又含有一个ViewPager2
解决方法二:在上一种方案的基础上,将内部视图的滑动阈值减小。针对场景中的例子,可以通过反射将内部ViewPager2的滑动阈值减一。由于ViewPager2不能被继承,因此也需要一个壳View来执行此操作,壳View代码如下:
/**
* 用来封装RecyclerView,其内部只能含有1个RecyclerView
*/
class SmallLinearLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
LinearLayout(context, attrs, defStyleAttr) {
val TAG = "SmallLinearLayout"
//记录视图中RecyclerView的滑动阈值,注:普通的RecyclerView与ViewPager2内部自带的RecyclerView的滑动阈值不同
var recyclerViewSlop = -1
constructor(context: Context):this(context,null)
constructor(context: Context,attributeSet: AttributeSet?):this(context,attributeSet!!,-1)
private fun setRecyclerViewTouchSlop(recyclerView: RecyclerView, touchSlop: Int) {
var clazz = RecyclerView::class.java
var field = clazz.getDeclaredField("mTouchSlop")
field.isAccessible = true
field.set(recyclerView,touchSlop)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
if(ev!!.action == MotionEvent.ACTION_DOWN){
var recyclerView = searchRecyclerView(this)
recyclerView?.let {
if(recyclerViewSlop==-1){
recyclerViewSlop = getRecyclerViewTouchSlop(it)
}
//减小reyclerView的滑动阈值
setRecyclerViewTouchSlop(it,recyclerViewSlop-1)
var slop = getRecyclerViewTouchSlop(recyclerView)
}
}
return super.dispatchTouchEvent(ev)
}
/**
* 获取ViewGroup中的RecyclerView
* @param vg ViewGroup
* @return RecyclerView?
*/
fun searchRecyclerView(vg:ViewGroup):RecyclerView?{
for(i in 0 until vg.childCount){
var view = vg.getChildAt(i)
if(view is RecyclerView){
return view
}else if(view is ViewGroup){
var recyclerView = searchRecyclerView(view)
if(recyclerView!=null){
return recyclerView
}
}
}
return null
}
fun getRecyclerViewTouchSlop(recyclerView: RecyclerView): Int {
var clazz = RecyclerView::class.java
var field = clazz.getDeclaredField("mTouchSlop")
field.isAccessible = true
var touchSlop = field.get(recyclerView) as Int
return touchSlop
}
}
将内部的ViewPager2方法此壳View里:
<com.example.icemusic.view.SmallLinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/ad_small_linear_layout"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/ad_view_pager2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onPageChange="@{(pos)->obj.onPageSelected(pos)}" />
</com.example.icemusic.view.SmallLinearLayout>
总结,针对2种不同场景下的滑动冲突问题,解决方法的主旨都是通过优先将触摸事件发送给内部View,然后在内部View不处理事件的情况下,外部View再进行处理。如果出现3或多个RecyclerView嵌套的情况,可以将上述2个壳view的代码组合使用。