事件分发机制主要是指触摸事件在Activity、ViewGroup、View之间传递并消费的机制,分发顺序为 Activity > ViewGroup > View;
主要方法:
VIewGroup相关:onInterceptTouchEvent()、dispatchTouchEvent()、onTouchEvent()
View相关:dispatchTouchEvent()、onTouchEvent()
1. Activity事件分发机制
一般情况下用户按下Activity时,Activity会执行dispatchTouchEvent方法,开始分发流程,我们看下dispatchTouchEvent()方法源码:
//activity的事件分发方法
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
dispatchTouchEvent方法里就两个处理:
(1)MotionEvent.ACTION_DOWN事件执行时会调用onUserInteraction()方法;我们看看这个方法做了啥;
public void onUserInteraction() {
}
这个方法实现是空的,不过我们可以从注释和其他途径可以了解到,该方法主要的作用是实现屏保功能,并且当此 Activity 在栈顶的时候,触屏点击 Home、Back、Recent 键等都会触发这个方法。
(2)接下来是if()语句执行了getWindow().superDispatchTouchEvent(ev)方法,能看出来他是获取了Window并调用了其superDispatchTouchEvent方法,但你会发现Window是个抽象类,superDispatchTouchEvent是个抽象方法,看不出任何东西,然后通过源码追踪你会发现其实getWindow获取的是Window的子类PhoneWindow;
final void attach(){
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
}
所以我们来看看PhoneWindow的superDispatchTouch方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
直接调用了DecorView的superDispatchTouchEvent方法,DecorView继承自FrameLayout,是顶层View,所有界面的父类,而FrameLayout是ViewGroup的子类,所以最终调用的是ViewGroup的dispatchTouchEvent方法。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
...
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
...
}
2. ViewGroup的事件分发机制
我们来看看ViewGroup的dispatchTouchEvent方法(Android5.0前的方法,5.0之后的方法比较复杂,但原理一样):
public boolean dispatchTouchEvent(MotionEvent ev) {
... // 仅贴出关键代码
// 重点分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
// 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
// b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断
// c. 关于onInterceptTouchEvent() ->>分析1
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 重点分析2
// 通过for循环,遍历了当前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
// 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
// 若是,则进入条件判断内部
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 条件判断的内部调用了该View的dispatchTouchEvent()
// 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
// 调用子View的dispatchTouchEvent后是有返回值的
// 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
// 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
// 即把ViewGroup的点击事件拦截掉
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
// 重点分析3
// 若点击的是空白处(即无任何View接收事件) / 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
// 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
// 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即自己处理该事件,事件不会往下传递(具体请参考View事件的分发机制中的View.dispatchTouchEvent())
// 此处需与上面区别:子View的dispatchTouchEvent()
}
...
}
(1)ViewGroup分发事件时首先判断当前Viewgroup有没有拦截事件onInterceptTouchEvent(ev),若返回值为true则表明已经拦截了此事件,跳出判断,事件不会往下传递,若是flase则相反,事件会往下传递。
(2)获取到ViewGroup的所有子View及子ViewGroup,通过for循环遍历;先判断次子View是否是正在点击的View(通过位置判断),如果是的话调用此子View的dispatchTouchEvent方法,这个方法是有返回值的,如果返回值为true就表明事件已被消费,跳出整个循环,不再往下传递,事件到此结束。
(3)如果点击的是空白处或事件被拦截的情况下会调用父类的dispatchTouchEvent()方法,即View.dispatchTouchEvent(),自己处理该事件,不会再往下传递。
3. View事件的分发机制
View的分发机制也是从dispatchTouchEvent()开始的,我们分析下源码;
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
方法里有一个if判断,里面有三个条件,若满足了这三个条件会返回true,即消费了该事件,否则执行onTouchEvent(),我们逐一分析这三个条件;
(1)mOnTouchListener != null,需要再View.setOnTouchListener()里注册Touch事件;
(2)(mViewFlags & ENABLED_MASK) == ENABLED,判断当前View是否enable,很多View默认enable;
(3)mOnTouchListener.onTouch(this, event) 回调以注册的Touch方法:
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
若onTouch返回了true,上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
若onTouch返回了false,上述三个条件不成立,执行onTouchEvent(event);
4. View.onTouchEvent()源码分析
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 若该控件可点击,则进入switch判断中
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
// a. 若当前的事件 = 抬起View(主要分析)
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
...// 经过种种判断,此处省略
// 执行performClick() ->>分析1
performClick();
break;
// b. 若当前的事件 = 按下View
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
// c. 若当前的事件 = 结束事件(非人为原因)
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
// d. 若当前的事件 = 滑动View
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
// 若该控件可点击,就一定返回true
return true;
}
// 若该控件不可点击,就一定返回false
return false;
}
/**
* 分析1:performClick()
*/
public boolean performClick() {
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
// 只要我们通过setOnClickListener()为控件View注册1个点击事件
// 那么就会给mOnClickListener变量赋值(即不为空)
// 则会往下回调onClick() & performClick()返回true
}
return false;
}
如果View是可点击的就返回true,否则返回false。
控件被点击
可以看出onTouch()的执行优先于onClick(),所以一旦在onTouch()消费了事件onClick()就不会执行。
5. 分发流程
分发流程6. 总结
事件分发从activity的dispatchTouchEvent方法开始往下遍历子View逐个寻找消费控件,如果有控件或布局消费了此事件,会执行这个控件或布局的onTouchEvent()方法,到此事件结束,不会再往下传递,如果没有控件或布局消费此事件,view层级从里到外逐个调用onTouchEvent()方法,直到activity自己消费此事件为止。
网友评论