背景
ViewPager2内嵌套横向滑动的RecyclerView,会有滑动冲突的情况,引入官方提供的NestedScrollableHost类可以解决冲突问题,但是有一些瑕疵,滑动横向RecyclerView到顶部,按住它不放手继续往左拖再往右拖,这时候会发现外层ViewPager2滑动了,而不是横向RecyclerView滑动,于是参考NestedScrollableHost进行逻辑完善
完整代码
- 主要是增加判断外层ViewPager2是否可滚动来设置是否允许父View拦截事件
open class NestRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
): RecyclerView(context, attrs) {
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 fun canViewScroll(target: View?, orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> target?.canScrollHorizontally(direction) ?: false
1 -> target?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
val orientation = parentViewPager?.orientation ?: return super.onInterceptTouchEvent(event)
if (!canViewScroll(this, orientation, -1f) && !canViewScroll(this, orientation, 1f)) {
return super.onInterceptTouchEvent(event)
}
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
initialX = event.x
initialY = event.y
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
val dx = event.x - initialX
val dy = event.y - initialY
val isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL
if (isVpHorizontal == dy.absoluteValue > dx.absoluteValue) {
parent.requestDisallowInterceptTouchEvent(false)
} else {
if (canViewScroll(this, orientation, if (isVpHorizontal) dx else dy)) {
parent.requestDisallowInterceptTouchEvent(true)
} else {
if (canViewScroll(parentViewPager, orientation, if (isVpHorizontal) dx else dy)) {
parent.requestDisallowInterceptTouchEvent(false)
} else {
parent.requestDisallowInterceptTouchEvent(true)
}
}
}
}
}
return super.onInterceptTouchEvent(event)
}
}
向上滑动AppBarLayout不联动问题
如果布局CoordinatorLayout + AppBarLayout + ViewPager2内嵌套横向滑动的RecyclerView,这时拖拽横向滑动的RecyclerView向上移,AppBarLayout不会跟着向上移
原因分析
-
拖拽横向滑动的RecyclerView向上移时,CoordinatorLayout.onNestedPreScroll内的lp.isNestedScrollAccepted(type)返回false,造成AppBarLayout没有执行scroll
-
lp.isNestedScrollAccepted(type)被赋值的地方,会根据AppBarLayout$Behavior.onStartNestedScroll返回的accepted进行赋值
-
AppBarLayout$Behavior.onStartNestedScroll内,会判断nestedScrollAxes的值不是2就返回false
-
RecyclerView也支持嵌套滑动。startNestedScroll是由NestedScrollingChildHelper实现的,它会将嵌套滑动上传,也就是NestedScrollingChild都会将嵌套滑动先交给NestedScrollingParent处理。
class RecyclerView
...
public boolean onInterceptTouchEvent(MotionEvent e) {
if (mLayoutSuppressed) {
// When layout is suppressed, RV does not intercept the motion event.
// A child view e.g. a button may still get the click.
return false;
}
// Clear the active onInterceptTouchListener. None should be set at this time, and if one
// is, it's because some other code didn't follow the standard contract.
mInterceptingOnItemTouchListener = null;
if (findInterceptingOnItemTouchListener(e)) {
cancelScroll();
return true;
}
if (mLayout == null) {
return false;
}
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(e);
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mIgnoreMotionEventTillDown) {
mIgnoreMotionEventTillDown = false;
}
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
if (mScrollState == SCROLL_STATE_SETTLING) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
stopNestedScroll(TYPE_NON_TOUCH);
}
// Clear the nested offsets
mNestedOffsets[0] = mNestedOffsets[1] = 0;
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
break;
...
return mScrollState == SCROLL_STATE_DRAGGING;
}
这里RecyclerView是横向的,所以nestedScrollAxis会被赋值为1,RecyclerView内调用startNestedScroll会向上层view传递,直到交给CoordinatorLayout处理,而CoordinatorLayout在调用onStartNestedScroll的时候,AppBarLayout$Behavior.onStartNestedScroll又返回false了,造成CoordinatorLayout回调onNestedPreScroll(由RecyclerView在ACTION_MOVE时调用dispatchNestedPreScroll触发)时无法调用AppBarLayout的滚动。
解决方法
在CoordinatorLayout调用onStartNestedScroll的时候不处理横向的情况,就不会导致lp.isNestedScrollAccepted(type)被赋值
class NestedCoordinatorLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
): CoordinatorLayout(context, attrs) {
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
return if (axes and ViewCompat.SCROLL_AXIS_HORIZONTAL != 0) {
false
} else super.onStartNestedScroll(child, target, axes, type)
}
}