关于View的事件我们都知道分三部分:DOWN,MOVE和UP,冲突是怎么产生的以及解决的思路是什么呢?
1、首先我们需要了解一下手机上的触摸点击事件是怎么来的,从用户手指点击到屏幕开始它的代码做了什么?首先是Activity的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
然后执行了getWindow().superDispatchTouchEvent(ev),getWindow()我们都知道其实就是PhoneWindow,在该类里面执行如下方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
mDecor其实就是DecorView,而DecorView继承了FrameLayout,FrameLayout又继承自ViewGroup,所以在DecorView里面我们看到执行的方法就是
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
最终执行到了ViewGroup里面的dispatchTouchEvent方法,因此我们需要分析的就是ViewGroup里面的事件处理,以上就是view的事件经过。
2、事件有分发和处理两种途径。
(1)先说下处理:如果我们的控件直接继承自View,例如TextView,ImageView。。。对于他们来说没有事件的分发,只有处理,当点击该控件的时候在View.java中直接执行DispatchTouchEvent方法,
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
、、、
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
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;
}
}
、、、
}
return result;
}
在这里面有 li.mOnTouchListener.onTouch(this, event) 和onTouchEvent(event)这两个语句就是我们平常信手拈来的setOnTouchListener里面的onTouch方法和setOnClickListener里面的onClick事件,如果我们的onTouch方法返回true,那么源码中的result 就为true 那么就不会执行onTouchEvent(event),因此我们的一个view在触发点击事件的时候先经历onTouch再经历onClick。
(2)事件分发:事件的分发是相对于容器来说的,例如一个FrameLayout,它是一个容器,当屏幕获取到触摸事件时它会判断该事件时需要去分发给它的children还是自己处理。然而事件的分发是在ACTION_DOWN,就是按下屏幕的时候,Activity就开始一级一级的询问容器
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
获取到intercepted = onInterceptTouchEvent(ev);,如果intercepted 为true,就是我们自定义容器的时候重写了onInterceptTouchEvent方法返回为true,就是当前的容器需要处理这个触摸事件,不会传递给它的children,最终调用View的dispatchTouchEvent处理该触摸事件,如果为false
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
就会去查找它的children,找到可以处理该事件的view,如果都没有那么该事件就会由它最近的一个夫容器处理。
3、冲突:事件的冲突是发生在MOVE的时候。例如:ViewPager + RecyclerView ,自定义一个ViewPager并重写onInterceptTouchEvent方法返回true的时候,发现RecyclerView不能上下滑动了,只有ViewPager左右滑动。
public class MyViewPager extends ViewPager {
public MyViewPager(@NonNull Context context) {
super(context);
}
public MyViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
}
当onInterceptTouchEvent返回false的时候Viewpager不能左右滑动,只能RecyclerView上下滑动。
解决此类冲突有两种:
第一种:内部解决冲突,就是自定义RecyclerView,使用requestDisallowInterceptTouchEvent()方法,当RecyclerView触发ACTION_DOWN的时候让Viewpager不可以拦截事件,当ACTION_MOVE的时候并且是水平滑动的时候让Viewpager拦截事件
public class MyRecyclerView extends RecyclerView {
int mLastX;
int mLastY;
public MyRecyclerView(@NonNull Context context) {
super(context);
}
public MyRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int)ev.getX();
int y = (int)ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltax = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltax) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
}
但是此种情况下 源码中有一个条件判断
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
因此我们需要在MyViewPager 中添加代码:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
super.onInterceptTouchEvent(ev);
return false;
}
return true;
}
第二种:外部解决冲突。就是处理父容器:
public class MyViewPager extends ViewPager {
private int mLastX;
private int mLastY;
public MyViewPager(@NonNull Context context) {
super(context);
}
public MyViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
// @Override
// public boolean onInterceptTouchEvent(MotionEvent ev) {
// if(ev.getAction() == MotionEvent.ACTION_DOWN){
// super.onInterceptTouchEvent(ev);
// return false;
// }
// return true;
// }
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = (int) ev.getX();
mLastY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int deltax = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltax) > Math.abs(deltaY)) {
Log.e("onInterceptTouchEvent:", "viewpager拦截了滑动事件");
return true;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev);
}
}
此种方案无须再处理RecyclerView。
结尾:在放一张view的分发事件图更加清晰的理解流程:
配合一段伪代码来理解上图:其实就一段递归的代码
/**
* @Title: dispatchTouchEvent
* @Description: 三者关系的伪代码
* @return: boolean
*
/
public boolean dispatchTouchEvent(MotionEvent ev){
//默认返回值
boolean consume = false;
//如果事件发生了拦截
if(onInterceptTouchEvent(ev)){
//消费事件
consume = onTouchEvent(ev);
}else{
//否则分发给子View
consume = child.dispatchTouchEvent(ev);
}
//返回值
return consume;
}