前言:
事件冲突在开发过程中经常碰到,比如说2个可以滑动的布局ViewPager和RecyclerView
这两个滑动的时候ViewPager左右滑动,RecyclerView上下滑动
本篇给大家由简单的冲突到常见的冲突,模拟滑动冲突进而跟进源码找到问题并解决问题!
事件分发流程
滑动冲突之前,先来温习一下事件分发流程吧~
在Android中当我们点击了屏幕首先会先执行到:
Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
在这里getWindow()获取的是Window对象,在Android中只有一个PhoneWindow继承子Window,所以直接在PhoneWindow里面找superDispatchTouchEvent()
PhoneWindow.java
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
这里的DecorView 继承自 FrameLayout , FrameLayout继承自ViewGroup,所以最终点击事件会发送到ViewGroup的dispatchTouchEvent()来进行事件分发
简单的滑动冲突
同一个按钮,设置setOnClickListener和setOnTouchListener不同的效果
bt.setOnClickListener(v -> {
Log.i("点击事件:", "OnClick");
});
bt.setOnTouchListener((v, event) -> {
Log.i("点击事件:", "OnTouchClick" + event.getAction());
return true;
}
);
结果图
:
可以看出setOnTouchListener设置返回值为true后,setOnClickListener()事件则不会进行处理
why?
走近View的dispatchTouchEvent()方法来一探究竟!
View.java
public boolean dispatchTouchEvent(MotionEvent event) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
....
}
在这个方法中咋们先看看li.mOnTouchListener.onTouch(this, event)是什么
public interface OnTouchListener {
boolean onTouch(View v, MotionEvent event);
}
这个onTouch(this, event)就是绿框选中的new View.OnTouchListener();
(红框 和 绿框的值是一样的,红框使用的是java8的特性lambda表达式)
在来看看setOnTouchListener做了什么事情:
View.java
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
看到这段代码,就应该明白,如果一旦调用了setOnTouchListener()那么传入的参数一定不为null,setOnClickListener()也是同样的道理
在来看一眼dispatchTouchEvent()方法
View.java
public boolean dispatchTouchEvent(MotionEvent event) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
....
}
在第一个if中判断都是用的&& ,既然onTouch()已经执行了,那么前面的条件一定是满足的.
如果li.mOnTouchListener.onTouch(this, event)
返回为true 那么一旦执行了if 最终 result = true
;
紧接着再来看第二个if判断,因为由第一个判断知道result = true
那么第二个if这段代码就不会执行也包括onTouchEvent(event)
在来看看onTouchEvent(event)方法中做了什么事情:
View.java
public boolean onTouchEvent(MotionEvent event) {
....
if (!post(mPerformClick)) {
performClickInternal();
}
....
}
View.java
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
return performClick();
}
View.java
public boolean performClick() {
...
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
....
return result;
}
最终在performClick()方法中发现了onClick()
在回过头来顺一下思路:
当点击事件来临的时候,会执行到View.dispatchTouchEvent()
然后会先判断通过mOnTouchListener.onTouch(this, event)判断是否将result变量赋值为true
如果为true就不执行onTouchEvent()方法,
onTouchEvent()方法调用了performClickInternal(),
performClickInternal()调用了 performClick()
onClick就在performClick()方法中
常见滑动冲突
布局ViewPager包含RecyclerView():
在这个小案例里面,ViewPager包含RecyclerView
所以ViewPager是ViewGroup, RecyclerView是View
MyViewPager.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev) / true / false
}
对应效果图:
super.onInterceptTouchEvent(ev)(默认) | true | false |
---|---|---|
咋们不管默认的,默认的google给处理好了,现在是模拟的冲突之后要如何解决
点击响应事件
首先要了解有那些事件onInterceptTouchEvent()会响应
ACTION_DOWN(0) | ACTION_MOVE(1) | ACTION_UP(2) | ACTION_CANCEL(3) |
---|---|---|---|
按压 | 滑动 (会响应多次) | 抬起 | 被父容器拦截 |
首先执行的就是ACTION_DOWN事件,DOWN事件是一切事件的开始,如果没有DOWN事件,就没有其他的事件
众所周知onInterceptTouchEvent()是是否拦截事件,true为拦截事件 false为不拦截事件
那么问题就来了,为什么返回了true就不能上下滑动了?
为什么返回了false不能左右滑动了?它的原理是什么?
接下来一步一步带领大家通过看源码的方式找到问题并解决问题!
当一个事件来临的时候还是会经过activity…phoneWindow…最终走到ViewGroup的dispatchTouchEvent()中,会先执行DOWN事件,然后在执行MOVE事件等
DOWN事件源码流程
注:源码很枯燥,一遍绝对看不懂,我2个小时候的视频看了4遍才总结的这么点,希望大家要耐心耐心耐心😘😘
在ViewGroup的dispatchTouchEvent()中:
- 首先会把ACTION_DOWN事件清空
ViewGroup.java
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
- 紧接着判断是否拦截事件
ViewGroup.java
//判断事件是否拦截 为DOWN事件的时候 disallowIntercept为false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//intercepted 为true 表示拦截事件 false表示不拦截事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
在这里需要注意的是onInterceptTouchEvent() 为true表示拦截事件 ,false表示不拦截事件
- 拦截事件 ------ start ------
onInterceptTouchEvent() 返回true:
ViewGroup.java
if (mFirstTouchTarget == null) {
//是否处理事件
//如果为false表示没有处理事件 true处理事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
dispatchTransformedTouchEvent()如果为false表示没有处理事件 true处理事件
ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
if (child == null) {
//会走到View的dispatchTouchEvent ViewGroup真正处理还是交给View
//handled 为false表示为处理事件 true表示处理事件
handled = super.dispatchTouchEvent(transformedEvent);
}
return handled;
}
最终这个事件交给View的dispatchTouchEvent()处理,就走到了文章最开头的那块代码,先判断OnTouch然后再onCilck等等
结论:如果ViewPager设置onInterceptTouchEvent为 true ,那么就不会走到事件分发直接事件拦截自己(ViewPager)消费了,所以他的子View(RecyclerView)就不会接收到事件
不接受事件,他就不会滑动!!
- 拦截事件 ------ stop ------
- 不拦截事件 ------ start ------
不拦截事件表示onInterceptTouchEvent为 false
再来接着看ViewGroup.dispatchTouchEvent()方法
如果不拦截事件那么就分发事件:
ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
//判断事件是否拦截 为DOWN事件的时候 disallowIntercept为false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//DOWN 事件一定可以进来 判断是否 拦截
intercepted = onInterceptTouchEvent(ev);
}
//如果 intercepted 为 false 则分发或处理事件
if (!canceled && !intercepted) {
...
//倒序取出
for (int i = childrenCount - 1; i >= 0; i--) {
....
//child.canReceivePointerEvents() 判断View能否接收事件
// isTransformedTouchPointInView() 判断是否在点击范围内
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
}
}
}
先来看看for循环里面的这个if
- child.canReceivePointerEvents() 判断View能否接收事件
View.java
protected boolean canReceivePointerEvents() {
//判断View能否接收事件
//1. 是否是VISIBLE状态
//2. 是否使用Animation移动了
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
- isTransformedTouchPointInView() 判断是否在点击范围内
ViewGroup.java
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
.....
return isInView;
}
什么是判断是否在点击范围内?
假设现在点击红色区域,应该响应的是红色区域,而不是绿色区域
在这个分发事件的时候,首先判断你是否是DOWN事件,
然后判断你是否有子View如果有然后倒叙排序(倒叙的原因是为了让最上层的View为第一个响应)
如果当前的View是VISIBLE(隐藏)状态或者通过Animation移动到了其他的位置,或者不再范围之内,那么就不分发
如果满足条件那么还是通过dispatchTransformedTouchEvent()分发
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){...}
- 判断是否在 分发处理事件 范围之内
- dispatchTransformedTouchEvent 如果为false继续循环出下一个
- dispatchTransformedTouchEvent 会询问他的子类是否需要处理事件
- 如果为false表示没有处理事件 true处理事件
- dispatchTransformedTouchEvent的关键在于第三个参数(child)
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
if (child == null) {
....
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (!child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
//如果子View 是ViewGroup 则会会继续执行ViewGroup的dispatchTouchEvent 相当于递归
//如果子View 是 View 则分发事件进行onTouchEvent处理
handled = child.dispatchTouchEvent(transformedEvent);
}
}
最终通过child.dispatchTouchEvent(transformedEvent);进行分发
- 如果子View 是ViewGroup 则会会继续执行ViewGroup的dispatchTouchEvent 相当于递归
- 如果子View 是 View 则分发事件进行onTouchEvent处理
结论:如果onInterceptTouchEvent()返回false,那么事件就会通过dispatchTransformedTouchEvent()全部分发给子View(RecyclerView)
- 不拦截事件 ------ stop ------
MOVE事件源码流程
执行完DOWN事件之后,紧接着就是执行MOVE事件,MOVE不分发事件
还是继续来看dspatchTouchEvent()方法
ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean alreadyDispatchedToNewTouchTarget = false;
//是否处理事件
if (mFirstTouchTarget == null) {
...
} else {
TouchTarget target = mFirstTouchTarget;
//这里while子循环一次
while (target != null) {
//第一次的时候next = null 因为 target.next 在addTouchTarget()中赋值为null
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
...
} else {
//最终在这里分发给子View事件
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {...}
else{
...
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
}
}
}
}
}
}
- alreadyDispatchedToNewTouchTarget默认为false,走到分发才会变为true,MOVE事件不分发事件
- whlie只会循环一次,因为target在addTouchTarget()中赋值为null,当第二次执行MOVE的时候就会被赋值,这里whlie循环的目的是为了多点MOVE事件
mFirstTouchTarget是一个链表 在这next第一次执行MOVE事件的时候
因为MOVE事件不走分发事件 next 是在分发事件中的 addTouchTarget()方法执行的
所以这个next = null = mFirstTouchTarget(红色部分)
当第二个MOVE事件来临的时候,就直接走(绿色部分)
if(mFirstTouchTarget == null)的事件分发了
解决思路
一切的拦截与分发,都是通过onInterceptTouchEvent()方法来改变的,那么如果可以不执行onInterceptTouchEvent()即可有办法解决
ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
//这里还有一个清除DOWN的方法
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//DOWN 事件一定可以进来 判断是否 拦截
//intercepted为true 表示拦截事件 false表示不拦截事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
但是咋们可以通过改变disallowIntercept 的值从而控制onInterceptTouchEvent()
disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
requestDisallowInterceptTouchEvent()介绍:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
...
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
...
}
- requestDisallowInterceptTouchEvent(true) 如果现在有事件存在,不让父容器拿到事件
- requestDisallowInterceptTouchEvent(false); 给父容器事件
然后还需要注意的是:
ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
//如果是DOWN事件 则吧之前状态清空
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
}
因为我们都知道,DOWN是所有事件的开始,如果没有DOWN事件,则没有MOVE事件,没有UP事件
都没有按压,如何移动,如何抬起?
所以在处理事件冲突的时候,在ViewGroup时,不拦截DOWN事件,将DOWN事件传递给子View,拦截MOVE事件
结论:ViewGroup不能拦截DOWN事件,事件冲突解决可以在子View的MOVE事件中进行
解决办法
内部解决法:
RecyclerView.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case ACTION_DOWN://按压
//不给父容器事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case ACTION_MOVE://移动
int moveX = x - mLastX;
int moveY = y - mLastY;
if (Math.abs(moveX) > Math.abs(moveY)) {
//给父容器事件
getParent().requestDisallowInterceptTouchEvent(false);
} else {
Log.i("ACTION_MOVE", "垂直滑动");
}
break;
case ACTION_UP: //抬起
break;
case ACTION_CANCEL://被父容器拦截
Log.i("ACTION_CANCEL", "RecyclerView");
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
ViewPager.java
//是否拦截事件 true 拦截事件 false不拦截事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(ev);
return false;
}
//在move事件的时候,拦截事件
return true;
}
外部拦截法
ViewPager.java
//外部拦截发
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
if (ev.getAction() == ACTION_DOWN) {
mX = (int) ev.getX();
mY = (int) ev.getY();
}
if (ev.getAction() == ACTION_MOVE) {
int moveX = mX - x;
int moveY = mY - y;
if (Math.abs(moveX) < Math.abs(moveY)) {
Log.i("ACTION_MOVE", "垂直滑动");
//如果是垂直滑动就不拦截
return false;
} else {
Log.i("ACTION_MOVE", "水平滑动");
}
}
return super.onInterceptTouchEvent(ev);
}
总结
-
1.先判断是否拦截 拦截走第三步,不拦截走第二步
-
2.不拦截,就分发 如果子view响应事件就走第四步
//如果没有进入分发 mFirstTouchTarget 会一直为null
//newTouchTarget = mFirstTouchTarget = null
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
- 3.拦截或分发处理
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//是否处理事件
//如果为false表示没有处理事件 true处理事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
4。不拦截:
所有子View都不拦截事件,最终走到第三步(拦截或分发处理)
重点:
-
通过 dispatchTransformedTouchEvent()分发给子View事件 如果为 false表示没有处理事件 true处理事件 false即子View的onTouchEvent返回false,ture也是同理
-
onInterceptTouchEvent() 是否拦截事件
-
mFirstTouchTarget是一个链表的TouchTarget
-
addTouchTarget()是当前事件的触摸目标(X,Y坐标)
-
MOVE不分发事件
-
处理事件冲突,只能在MOVE事件时处理
-
getParent().requestDisallowInterceptTouchEvent(true); 允许父容器拿到事件
-
getParent().requestDisallowInterceptTouchEvent(false); 不允许父容器拿到事件
原创不易,您的点赞就是对我最大的支持~