最近看了《Android开发艺术探索》,记下笔记
Android事件类型
事件类型主要ACTION_DOWN、ACTION_MOVE、ACTION_UP三种,一个事件序列,包含了一个ACTION_DOWN、若干个ACTION_MOVE和一个ACTION_UP。
事件传递的三个重要方法
public boolean dispatchTouchEvent(MotionEvent event)
用来事件分发,返回true,则表示消耗当前事件,返回值受当前view的onTouchEvent和子View的dispatchTouchEvent影响
public boolean onInterceptTouchEvent(MotionEvent ev)
在上诉方法内部调用,表示是否拦截事件,这个方法只在ViwGroup存在;如果返回true,则事件部分发给子View,事件会交给自身的onTouchEvent()处理
public boolean onTouchEvent(MotionEvent event)
表示事件是否消耗,返回true,表示当前view消耗事件,返回false,则事件交给父视图的onTouchEvent()处理
以下伪代码可以形象的表现三者的关系:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean dis = false;
if(onInterceptTouchEvent(ev)){
dis = onTouchEvent(ev);
}else{
dis = child.dispatchTouchEvent(ev);
}
return dis;
}
事件传递的结论
从Android开发艺术探索书中摘录整理了一些结论:
- 某个view一旦决定拦截,那么这个事件序列都交给它来处理,它的onInterceptTouchEvent不会再调用
- 事件传递由dispatchTouchEvent开始,事件会按照嵌套层次由外向内传递,传递到最内层view时,交由它的onTouchEvent处理,该方法如果消费该事件,则返回true,如果不消费则返回false,这时事件会向外层传递,交给它的父视图的onTouchEvent处理,以此类推
- viewGroup默认不拦截任何事件,源码中onInterceptTouchEvent默认返回false
- view的onTouchEvent默认都会消耗事件(返回true),除非他是不可点击的(clickable和longClickable同时为false)view的longClickable默认都为false,clickable要看情况,比如Button默认为true,TextView默认为false
- view的事件触发顺序是先执行onTouch方法再执行onTouchEvent再执行onClick方法,如果onTouch返回true,则onClick不会调用
- Activity中事件的传递顺序是Activity—>Window—>根View—>布局对应的ViewGroup—>View
ViewGroup中事件的传递
先看ViewGroup的dispatchTouchEvent方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
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 {
intercepted = true;
}
由上面代码可以看出,有两种情况会判断是否拦截当前事件(调用onInterceptTouchEvent):事件类型为ACTION_DOWN或者mFirstTouchTarget != null。那么mFirstTouchTarget 啥时候不为null呢,答案是当前事件不拦截,事件交给子view时。那么反过来,如果当前view拦截事件,则mFirstTouchTarget为null,后续事件为MOVE和UP事件,则actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null都为false,这也印证了第1条结论:当前view拦截当前事件,则事件序列后续的其他事件都交给处理,onInterceptTouchEvent不会再调用。
再看onInterceptTouchEvent调用前的一个判断,事件拦截方法还受FLAG_DISALLOW_INTERCEPT标志位影响,这个标志位可以通过requestDisallowInterceptTouchEvent方法修改,这个方法可以用来处理滑动冲突。
viewGroup不拦截事件,事件会交给他的子View处理,继续看它的dispatchTouchEvent方法:
if (!canceled && !intercepted) {
...
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
//****判断view能否接受到点击事件,不能就取下一个view判断
ev.setTargetAccessibilityFocus(false);
continue;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//****如果子view的dispatchTouchEvent()返回true,addTouchTarget中会对mFirstTouchTarget进行赋值,并跳出循环
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
代码做了精简,由代码可以看出这部分逻辑:
- 如果ViewGroup不拦截事件,则先遍历了它的子元素,然后判断子元素是否能获取到点击事件,衡量标准是:子元素是否在播放动画和点击事件的坐标是否在子元素区域内(具体逻辑可以看canViewReceivePointerEvents和isTransformedTouchPointInView的实现)。
- 子元素可以接收到点击事件,则调用dispatchTransformedTouchEvent,注意第三个参数child不为null,这个方法实际就是调用了子View的dispatchTouchEvent,至此事件被分发给子View.
看下dispatchTransformedTouchEvent的具体实现
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
boolean handled;
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return handled;
如果子view的dispatchTouchEvent返回true,则dispatchTransformedTouchEvent返回true,我们继续回到viewGroup事件不拦截处理逻辑中看,我只把核心逻辑摘抄出来:
for (int i = childrenCount - 1; i >= 0; i--) {
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//****如果子view的dispatchTouchEvent()返回true,addTouchTarget中会对mFirstTouchTarget进行赋值,并跳出循环
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
dispatchTransformedTouchEvent返回为true,会调用addTouchTarget方法,他里面干了啥呢?我们看看他的具体实现:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
由上面代码我们可以得出结论,事件不拦截时,会遍历viewGroup中的元素,如果某个子view答应处理,即dispatchTouchEvent返回true,则跳出循环,此时事件已经交给子view处理,子view再执行这样的分发逻辑,从而完成事件由外向内分发;第二点当前事件不拦截时mFirstTouchTarget !=null,拦截时,mFirstTouchTarget ==null,不再调用onInterceptTouchEvent方法。
继续往下看,在viewGroup遍历子view逻辑后有如下判断逻辑:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
该代码表示遍历完所有子元素,事件都没有被处理(ViewGroup没有子元素或子元素都不处理),则再次调用dispatchTransformedTouchEvent方法,注意此时该方法的第三个参数为null,由dispatchTransformedTouchEvent内部实现可知,此时会调用super.dispatchTouchEvent(event);即事件交给view来处理。由此我们可以得出结论:1、事件由外层向内层通过dispatchTransformedTouchEvent分发,如果内层的dispatchTransformedTouchEvent返回true,则代表内层view消耗事件;如果viewGroup没有内层view或内层所有子view都不处理事件,则事件由他自己处理。这就像生活中经理有个任务需要处理,先交给主管,主管再交给组员处理,如果组员能处理,则主管就不用处理了,如果组员处理不了,那就只能主管再处理,如果主管也处理不了,就只能经理自己处理了。还有一种可能,经理下面没有人,那任务分发不出去,只能经理自己处理了。
至此,ViewGroup的分发和拦截逻辑已经分析完,接下来看view的dispatchTouchEvent和onTouchEvent方法
View中事件的传递
从view的事件分发开始看起
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;
}
}
view的事件分发就简单很多,由于view不是一个容器,因此没有子元素,不存在事件的向内传递,事件需要它自己处理。我们看到它首先判断有没有mOnTouchListener ,如果由则调用mOnTouchListener #onTouch方法,而onTouch方法返回true,则result=true,则onTouchEvent不会调用,由此我们得出结论:onTouch优先级高于onTouchEvent。
接着看onTouchEvent的实现:
public boolean onTouchEvent(MotionEvent event) {
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!post(mPerformClick)) {
performClickInternal();
}
...
break;
...
return true;
}
private boolean performClickInternal() {
...
return 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;
}
由上面代码可知,只要view的CLICKABLE和LONG_CLICKABLE有一个为true,那么他就会消耗这个事件,即onTouchEvent返回true,不管他是不是DISABL状态。然后就是当ACTION_UP事件发生时会触发performClick,如果view设置了OnClickListener,那么最终会调用它的onClick方法。结合前面的结论,我们就可以印证第4、5条结论。到这里事件的分发机制源码已经分析完了。
链接:
解决这 8 个问题,Android事件分发再往前一步
Android View的事件分发机制和滑动冲突解决方案
Android事件分发机制详解:史上最全面、最易懂