简介
滑动冲突,简单来说就是两个可滑动的组件嵌套在一起,其中一个组件拦截了滑动事件,导致另一个组件无法滑动或难以滑动的情况。
常见的情况有ViewPager2嵌套ViewPager2,ViewPager2嵌套RecyclerView。
官方解决方案
谷歌官方提供了一种解决同方向滑动冲突的方案 ——NestedScrollableHost.kt
将这个布局放在ViewPager2和RecyclerView之间即可解决滑动冲突,类似这样:
<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager2.widget.ViewPager2
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.miui.gallery.widget.tsd.NestedScrollableHost
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>
</com.miui.gallery.widget.tsd.NestedScrollableHost>
</androidx.viewpager2.widget.ViewPager2>
简单分析一下NestedScrollableHost是如何处理滑动冲突的:
- 获取父布局ViewPager2,ViewPager2可以不是其直接父布局。
- 获取子布局RecyclerView,RecyclerView必须是其第一个子View。
- 处理滑动事件,判断滑动方向:
- 不是RecyclerView的滑动方向,不做任何处理,Return。
- 是RecyclerView的滑动方向,判断RecyclerView能否继续滑动,若可以,则禁止ViewPager2拦截此事件;反之,允许ViewPager2拦截此事件。
原理十分简单,无非就是:如果RecyclerView仍能处理事件,则由它处理,反之由ViewPager2处理。
从解决方案来看,该方案提供了一种不错的思路,但并不完全适用于各种开发情景,对于我所做的需求而言,仍存在以下两个痛点:
- RecyclerView必须是NestedScrollableHost的第一个子View。
- 只解决同方向的滑动冲突。
那么,就针对这两个问题,对NestedScrollableHost进行一个简单的改造吧。
完善解决方案
提供遍历子View的方法
针对痛点1,我希望NestedScrollableHost无论下层布局如何,都能找到其中的RecyclerView,那么仅需提供一个遍历子View的方法即可。
public static View getChildRecyclerView(View view) {
ArrayList<View> unvisited = new ArrayList<>();
unvisited.add(view);
while (!unvisited.isEmpty()) {
View child = unvisited.remove(0);
if (child instanceof RecyclerView) {
return child;
}
if (!(child instanceof ViewGroup)) {
continue;
}
ViewGroup viewGroup = (ViewGroup) child;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
unvisited.add(viewGroup.getChildAt(i));
}
}
return null;
}
针对不同滑动方向的处理
针对痛点2,需要为NestedScrollableHost额外添加一条属性,以此属性来判断当前的ViewPager2与RecyclerView的滑动方向是否一致,并在代码中修改相关的逻辑。
<declare-styleable name="NestedScrollableHost">
<attr name="sameDirectionWithParent" format="boolean" />
</declare-styleable>
<com.mone.NestedScrollableHost xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:sameDirectionWithParent="false">
</com.mone.NestedScrollableHost>
完整代码
虽然进行了一些调整,但整体的思路不变:如果RecyclerView仍能处理事件,则由它处理,反之由ViewPager2处理。
class NestedScrollableHost : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
val a = context.obtainStyledAttributes(attrs, R.styleable.NestedScrollableHost)
isChildHasSameDirection =
a.getBoolean(R.styleable.NestedScrollableHost_sameDirectionWithParent, false)
a.recycle()
}
private var isChildHasSameDirection = true
private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
while (v != null && v !is ViewPager2) {
v = v.parent as? View
}
return v as? ViewPager2
}
private val child: View? get() = ViewUtils.getChildRecyclerView(this)
init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
val childOrientation = if (isChildHasSameDirection) orientation else orientation xor 1
if (!canChildScroll(childOrientation, -1f) && !canChildScroll(childOrientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDx > scaledDy)) {
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
parent.requestDisallowInterceptTouchEvent(true)
} else {
parent.requestDisallowInterceptTouchEvent(false)
}
} else {
parent.requestDisallowInterceptTouchEvent(true)
}
}
}
}
}
|