在源码分析的基础上进行滑动冲突的解决
2021-11-08
6 min read
根据前面进行的事件分发的源码分析,现在我们进行滑动冲突的解决。事件的冲突是只发生在ACTION_MOVE中的,根据解决冲突位置的不同,可以有两种解决方案:
在处理冲突之前,先回顾一下ViewGroup中事件分发中拦截的流程:
// Check for interception.
//是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
/**子view调用requestDisallowInterceptTouchEvent来修改是否允许拦截,如果不允许拦截的话,会进入if,
这个时候就要看ViewGroup的onInterceptTouchEvent的值返回是什么,但是事件的分发是在子View之前,
ACTION_DOWN的时候会重置mGroupFlags为 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;所以disallowIntercept为false会走if的判断,需要父布局处理dowan的事件拦截判断。*/
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
/**如果是拦截的话,第一次的move只会重置相应的事件,mFirstTouchTarget置为null,并且会给子view发一个ACTION_CANCEL的事件,
这里就相应之前的说法,子View的ACTION_CANCEL事件只有在父布局拦截的时候会触发,拦截后下面会走dispatchTransformedTouchEvent自己进行处理*/
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
所以整个冲突的解决思路就是要在合适的时候让合适的对象去处理事件,并且这里处理的原则是在同一个事件链中只相应一个方向的事件,例如ViewPage嵌套ListView,这里会有两个方向的滑动,也就是在响应Viewpage横向事件时,不再响应ListView的竖向的事件,反之亦然。
第一种:内部解决
内部结局也就是子控件来控制整个流程中应该由谁来响应事件,是子View自己来处理事件还是父布局来进行拦截(拦截也就是由父布局进行事件的处理)。
因为ViewGroup的事件分发中的事件链第一个ACTION_DOWN会查找能够响应事件的view,所以我们需要保证在ACTION_DOWN时子view调用requestDisallowInterceptTouchEvent(true)来请求时,ViewGroup的onInterceptTouchEvent要返回false。
父布局:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN){
super.onInterceptTouchEvent(event);
//在ACTION_DOWN时要拦截事件
return false;
}
return true;
}
子View:
//内部拦截法:子view处理事件冲突
private int mLastX, mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.e("======", "ACTION_DOWN");
// mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
//之后mGroupFlags & FLAG_DISALLOW_INTERCEPT
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
//这里就是如果横向事件满足那么直接进行拦截不再响应事件
if (Math.abs(deltaX) > Math.abs(deltaY)) {
Log.e("======", "requestDisallowInterceptTouchEvent false");
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
第二种:外部处理
也就是父布局进行事件判断,当前是否分发给子View
父布局:
// 外部拦截法:父容器处理冲突
// 我想要把事件分发给谁就分发给谁
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mLastX = (int) event.getX();
mLastY = (int) event.getY();
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
//横向移动距离大于竖向移动距离,那么拦截事件不再分发
if (Math.abs(deltaX) > Math.abs(deltaY)) {
return true;
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
return super.onInterceptTouchEvent(event);
}
针对一个实际🌰进行处理
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<com.enjoy.srl_vp.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.enjoy.srl_vp.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.enjoy.srl_vp.SwipeRefreshLayout>
</android.support.constraint.ConstraintLayout>
一个外层布局是SwipeRefreshLayout,内部ViewPage,ViewPage内部为Fragemnt
外部解决方案:
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
if(ev != null){
when(ev.action){
MotionEvent.ACTION_DOWN->{
mLastX = ev.x.toInt()
mLastY = ev.y.toInt()
}
MotionEvent.ACTION_MOVE->{
val delX = ev.x - mLastX
val delY = ev.y-mLastY
//横向滑动不拦截事件
if(Math.abs(delX) >Math.abs(delY)){
return false
}
}
}
}
return super.onInterceptTouchEvent(ev)
}
内部解决方案:
父View(SwipeRefreshLayout):
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//还是只需要处理ACTION_DOWN事件
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(ev);
return false;
}
return true;
}
}
子View(ViewPager):
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = ev.getX();
startY = ev.getY();
//这句话如果不加的话处理是不会生效的,所以我们需要看一下SwipeRefreshLayout的requestDisallowInterceptTouchEvent是不是有什么特殊处理
ViewCompat.setNestedScrollingEnabled(this, true);
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
x = ev.getX();
y = ev.getY();
deltaX = Math.abs(x - startX);
deltaY = Math.abs(y - startY);
if (deltaX < deltaY) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
SwipRefreshLayout:
public void requestDisallowInterceptTouchEvent(boolean b) {
/**
条件单独分析
21没问题
前面的条件过
包含有子View这里的target不为null
所以就需要看这个了 ViewCompat.isNestedScrollingEnabled(this.mTarget))
我们获取ViewPage的isNestedScrollingEnabled的值为false,所以这个就没有进入if条件内
所以通过 ViewCompat.setNestedScrollingEnabled(this, true);this就是当前的内部view
*/
if ((VERSION.SDK_INT >= 21 || !(this.mTarget instanceof AbsListView)) && (this.mTarget == null || ViewCompat.isNestedScrollingEnabled(this.mTarget))) {
super.requestDisallowInterceptTouchEvent(b);
}
}