前言:
事件分发作为android面试必问的面试点,我们是很有必要熟练掌握以及理解的,很多时候我们都知道整个分发流程,先后顺序。这在面试的时候便足够应付。但每当我们在实际开发中,切实遇到这类问题时,理论就发挥不了作用,解决不了问题,所以这篇文章不会介绍事件分发的具体流程,而是从源码的角度来一探究竟view的事件分发机制到底是怎么完成的。
大纲
- 事件分发的关键方法
- onTouch onClick之间的关系 分析源码了解原因
- View的事件分发机制
- 事件冲突的原因,如何解决冲突
事件分发的关键方法:
ACTION_DOWN 手指初次接触到屏幕时触发
ACTION_MOVE 手指在屏幕上滑动时触发,会多次触发
ACTION_UP 手指离开屏幕时触发
ACTION_CANCEL 事件被上层拦截时触发
事件分发 :dispathTouchEvent
事件拦截: onInterceptTouchEvent
事件消费: onTouchEvent
onTouch onClick之间的关系 分析源码了解原因
我们应该都被面试官问过这样一个问题,onTouch和onClick的执行顺序,当onTouch 返回ture的时候,onclick 是接收不到的,只有当onTouch 返回false时,onClick才有回调,面试的时候一般这样说其实就可以过了,但我们今天便通过对源码分析来探究为什么会这样。
首先思考一个问题,当我们点击button,同时给button设置 onclick 或者 onTouch 事件以后,这个事件是怎么被回调回来的呢
Button button = findViewById(R.id.main_settings);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//我是怎么被回调的呢
}
});
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
//我是怎么被回调的呢
return false;
}
});
上文说到过,事件分发需要调用dispathTouchEvent方法。那么我们带着这个问题来看看dispathTouchEvent里面做了什么,因为button没有重写dispatchTouchEvent,可以查看父类view的实现。(只贴出关键代码)
public boolean dispatchTouchEvent(MotionEvent event) {
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;
}
我们直接来看第四行,mListenerInfo 把自身赋值给了li,
第五行,li != null && li.mOnTouchListener != null li和li.mOnTouchListener 不为空返回 true
第六行,判断当前点击事件是否是可点击,肯定为ture不然事件也不会走到这里。
第七行,关键方法,i.mOnTouchListener.onTouch 调用onTouch 方法,是否为true在于onTouch的返回值,而这个返回值就是我们在设置button的OnTouchListener时的回调。
所以这个if语句问题关键在于 怎么确保mListenerInfo 不为空,以及li.mOnTouchListener不为空
我们回过头来,看看button.setOnTouchListener()这个方法做了什么
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
点开getListenerInfo()
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
这里是一个单例判断,如果mListenerInfo为空new一个新的,不为空返回当前的。所以我们可以知道mListenerInfo一定不会空,第五行一定返回true。
继续往下看,当我们的onTouch返回true时,
第十一行,!result 返回false, 我们知道&&的特性,当!result为false时, onTouchEvent便不会执行,而其实我们的onclick事件便是在onTouchEvent中回调,所以这便是为什么onTouch返回ture时,onClick不会执行的根本原因。
(onTouchEvent中,onClick事件是在ACTION_UP 时候被回调,在 ACTION_UP 中会调用performClickInternal()方法,而这个方法内部会返回performClick() ,可以看看performClick实现)
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
其实和onTouch的判断差不多,判断通过,回调onClick事件。有兴趣的朋友可以自己跟着源码看一下。
View的事件分发机制
相信每个android开发者开发过程中,都会遇见各种各样的事件冲突,如果不了解其中的根源,解决起来肯定头大,各种调试,百度的结果让人心累。而当我们懂的事件冲突的根本原因,定位问题,解决问题时便游刃有余。
任何一个事件的分发都是从Activity开始,Activiyt之上都是c++层处理,我们无需关心。一般情况下,事件都是从用户按下(ACTION_DOWN)的那一刻产生的,当点击事件产生后,事件首先会传递给当前的 Activity,所以我们就从Activity的dispathTouchEvent开始,看看源码中是怎么来处理事件分发的。
-
activity的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
我们直接看第二个if语句,getWindow().superDispatchTouchEvent(ev),第一个是给开发者重写的一个空方法,主要是屏保功能,可以不关心,getWindow返回window,而window只有一个实现类,就是phoneWindow,我们来看
-
phoneWindow的superDispatchTouchEvent:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
返回一个mDecor, mDecor就是我们熟悉的DecorView,继续往下
-
DecorView的superDispatchTouchEvent:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
Decorview父类是ViewGroup,所以最终调到了ViewGroup的dispatchTouchEvent,因为事件分发核心都在这个方法里面,内容较多,我们将一点点展现出来。
-
我们都知道一个事件的发生是从Down事件开始,所以我们就先来分析一下dispatchTouchEvent方法中,down事件是如何进行的。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
//判断是否是Down事件,如果是,做一些重置工作
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
//这个方法会让requestDisallowInterceptTouchEvent无效
resetTouchState();
}
// 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;
}
重点关注第16行开始的代码,首先会判断当前事件是否为Down,然后得到一个布尔值disallowIntercept,如果disallowIntercept为true则不会执行拦截方法onInterceptTouchEvent。这里disallowIntercept这个值我们重点介绍一下,我们知道事件分发中有这么一个函数requestDisallowInterceptTouchEvent,请求父类不要拦截事件,我们来看看这个函数做了什么。
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
当我们在view中设置了requestDisallowInterceptTouchEvent为true的时候,这个方法中会将mGroupFlags赋值,从而使得 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0返回true; 也就是disallowIntercept 为ture,导致我们的onInterceptTouchEvent没办法执行,所以这也是平时我们设置了这个方法父类不会拦截子类的原因。但是这里面有一个很大的坑。
当我们当前的事件为Down的时候,requestDisallowInterceptTouchEvent是不生效的。因为上面说到的第9行,判断当前事件是否是Down事件,如果是,会调用resetTouchState方法
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
当事件为down时,这个方法会执行 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;,从而导致disallowIntercept 为false,所以还是会执行父类的onInterceptTouchEvent拦截方法。
继续往下看,因为篇幅原因 大概写一下这个if语句中重要的一些方法,并加上注释。
if (!canceled && !intercepted) {
//对子View进行排序,拿到当前GropuView下一层View,而不是所有view
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
//拿到最外层view
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//判断点击事件
if (!canViewReceivePointerEvents(child)
//判断clid的位置是否是点击位置
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
//如果不符合,重新遍历取下一个child
ontinue;
}
//分发给哪个子View处理事件
(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
//addTouchTarget方法,
//target = TouchTarget.obtain(child, pointerIdBits);
//mFirstTouchTarget = null;
//target.next = mFirstTouchTarget;
//mFirstTouchTarget = target;
//所以 target.next == null, mFirstTouchTarget !=null 返回了 mFirstTouchTarget
addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
}
这个if是整个事件分发中最为关键的地方,会对所有子view排序,然后遍历。并且其中的几个变量值也影响接下来的事件分发。
// 如果为down事件 mFirstTouchTarget不为空看else
// 如果为move事件 则交给自己的dispatchTouchEvent处理,具体逻辑可看dispatchTransformedTouchEvent
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
//down事件 next为空上文已经分析
final TouchTarget next = target.next;
// 两个条件都为true,上文已经分析,所以返回handled = true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//down事件到此就结束了
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//所以会走到这里来,dispatchTransformedTouchEvent 分发事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
// target为空,跳出while循环
target = next;
}
}
以上便是一个down事件的流程,其实无论是down还是move都是需要通过dispatchTouchEvent分发事件,而move事件是可以反复调用此方法的,当一个move事件进来的时候是无法进行分发的,因为此时已经通过down确定了哪个子view来消耗此次事件。最终move逻辑上文也已经注解了。
事件冲突的原因,如何解决冲突
以上便是事件分发的流程,只有搞懂了这些流程,在实际开发中才能解决一些冲突问题。 那么这些事件为什么会冲突。知道流程以后又如何解决呢。
为了更清晰的描述事件冲突,我们有这么一个例子,Viewpager嵌套ListView。我们重写Viewpager的onInterceptTouchEvent方法,无论该方法返回true还是,还是false,Viewpager的左右滑动事件便和ListView上下滑动事件冲突。但是我们去掉这个方法时,冲突事件便没有了。
当onInterceptTouchEvent返回true时,intercepted就为true,从而导致if (!canceled && !intercepted) {}方法不会发生。因此mFirstTouchTarget就为空。
当onInterceptTouchEvent返回false时,
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;
}
//当事件为move onInterceptTouchEvent返回true不会进到if中
//当事件为move onInterceptTouchEvent返回false不会进到for循环中
//所以无论无何都不会进行事件分发
//当事件为down 我们拦截事件时候,
if (mFirstTouchTarget == null) {
//onInterceptTouchEvent返回true
//dispatchTransformedTouchEvent 方法传入的child为空
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//onInterceptTouchEvent返回false
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//当事件变成move的时候,alreadyDispatchedToNewTouchTarget被置为false
//dispatchTransformedTouchEvent 会调用到listview里面去,所以Viewpager左右滑动无效
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
//如果child为空,调用自己的 dispatchTouchEvent ,所以不会执行listview的事件
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
那么如何解决这些冲突呢。两种方法
-
内部拦截法
重写Viewpager的onInterceptTouchEvent
@Override public boolean onInterceptTouchEvent(MotionEvent event) { //内部拦截发 if (event.getAction() == MotionEvent.ACTION_DOWN) { super.onInterceptTouchEvent(event); return false; } return true; }
重写ListView的dispatchTouchEvent
//内部拦截法 @Override public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.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; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); }
-
外部拦截法
重写Viewpager的onInterceptTouchEvent
@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); }