基于Android9.0,了解Android事件分发
还是那句话:点成线,线成面,切勿贪心,否则一脸懵逼
先记住这个事件分发的顺序:
Activity->ViewGroup->View
以及三个重要的方法:
方法名 | 作用是什么? | 什么时候调用? |
---|---|---|
dispatchTouchEvent() | 传递(分发)事件 | 当前View能够获取点击事件时 |
onTouchEvent() | 处理点击事件 | 在dispatchTouchEvent()内部调用 |
onInterceptTouchEvent() | 判断是否拦截事件 $\color{red}{注意:只存在于ViewGroup中}$ |
在ViewGroup的dispatchTouchEvent()中调用 |
脑海里大概有了这个顺序和概念,我们就从源码开始吧。
当触发点击事件时,最先响应的是
Activity的dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {//按下事件
onUserInteraction();//这是一个空方法。activity无论分发按键事件、触摸事件或者轨迹球事件都会调用Activity#onUserInteraction()。
}
if (getWindow().superDispatchTouchEvent(ev)) {//获取当前Window,Window是一个abstract类,这里它的实现是PhoneWindow
//Window的superDispatchTouchEvent方法,也是一个abstract方法,所以要去看PhoneWindow的superDispatchTouchEvent
return true;
}
return onTouchEvent(ev);//没有任何View 接收/处理事件,调用自身的onTouchEvent
}
PhoneWindow的superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);//mDecor为DecorView实例,DecorView为PhoneWindow的顶层Window
}
DecorView继承于FrameLayout,而FrameLayout继承于ViewGroup,所以,DecorView是ViewGroup的子类。
//DecorView的superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
这里super.dispatchTouchEvent(event);其实就是ViewGroup的dispatchTouchEvent。这里先不作深入分析,假设它返回false,先把第一条路走通,然后再来分析ViewGroup的dispatchTouchEvent。
最后调用onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
上面我们知道mWindow就是PhoneWindow,但是shouldCloseOnTouch不是abstract方法,它的实现在Window里
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;//点击Window外面
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {//peekDecorView为DecorView
return true;
}
return false;
}
private boolean isOutOfBounds(Context context, MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop();//16像素
final View decorView = getDecorView();
return (x < -slop) || (y < -slop)
|| (x > (decorView.getWidth()+slop))
|| (y > (decorView.getHeight()+slop));
}
最后返回false,就代表没有消费事件。
ViewGroup的dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//disallowIntercept:是否禁用事件拦截的功能。默认为false,不禁用
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;
}
...
if (!canceled && !intercepted) {//没有取消也没有拦截
...
for (int i = childrenCount - 1; i >= 0; i--) {
...
if (!child.canReceivePointerEvents()//是否响应
|| !isTransformedTouchPointInView(x, y, child, null)) {//是否在View范围内
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
...
//注意dispatchTransformedTouchEvent方法,分发事件给子View的关键
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//在addTouchTarget里面,mFirstTouchTarget被赋值
alreadyDispatchedToNewTouchTarget = true;
break;
}
。。。
}
if (preorderedList != null) preorderedList.clear();
}
...
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//注意dispatchTransformedTouchEvent方法,分发事件给子View的关键
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//注意dispatchTransformedTouchEvent方法,分发事件给子View的关键
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
...
}
}
...
}
...
return handled;
}
先来看看onInterceptTouchEvent方法,最后再看dispatchTransformedTouchEvent这个方法
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;//默认返回false
}
既然ViewGroup默认不拦截,那就来看看dispatchTransformedTouchEvent是如何分发的
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {//子View为空,调用自己的dispatchTouchEvent
handled = super.dispatchTouchEvent(event);
} else {//子View不为空,调用子View的dispatchTouchEvent
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
handled = child.dispatchTouchEvent(event);
...
}
return handled;
}
...
} else {
...
}
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
...
return handled;
}
其实逻辑不复杂,核心思想就是child==null,调用自身父类的dispatchTouchEvent,如果不为空,就调用子View的dispatchTouchEvent,接下来,我们就来看看View的dispatchTouchEvent。
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//这里OnTouchListener,就是平常我们设置的setOnTouchListener,onTouch自然就是重写的onTouch
result = true;
}
if (!result && onTouchEvent(event)) {//根据需求,可以重写onTouchEvent。onTouchEvent默认返回true
result = true;
}
}...
return result;
}
到这里,事件分发基本就结束了,是不是脑子有点糊?胖子第一次也是的,不过,加上自己写个demo,会清楚很多。下面我们来敲敲demo,加深这条线的印象。
首页创建MainActivity、CustomRelativeLayout、CustomView,然后分别重新他们的dispatchTouchEvent、onInterceptTouchEvent(CustomRelativeLayout独有)、onTouchEvent,加上日志,最后看看xml和CustomView的监听
<com.example.test.CustomRelativeLayout
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.test.CustomView
android:id="@+id/textTv"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
android:background="@color/colorAccent" />
第一种情况:事件不消费
activityMainBinding.textTv.setOnTouchListener((v,event)->{
Log.e(TAG,"textTv --->> setOnTouchListener");
return false;//注意这里,默认是返回false。表示不消费
});
// activityMainBinding.textTv.setOnTouchListener(new View.OnTouchListener() {
// @Override
// public boolean onTouch(View v, MotionEvent event) {
// return false;
// }
// });
点击CustomView看看日志
E/MainActivity: 调用dispatchTouchEvent第0次 //点击,从上往下传递
E/MainActivity: dispatchTouchEvent //先调用Activity的事件分发
E/CustomRelativeLayout: dispatchTouchEvent //再调用ViewGroup的事件分发
E/CustomRelativeLayout: onInterceptTouchEvent//判断ViewGroup是否拦截事件
E/CustomView: dispatchTouchEvent//View的dispatchTouchEvent
E/MainActivity: textTv --->> setOnTouchListener//View的setOnTouchListener,onTouch返回false
E/CustomView: onTouchEvent//从下往上返回
E/CustomView: onTouchEvent ACTION_DOWN
E/CustomRelativeLayout: onTouchEvent
E/CustomRelativeLayout: onTouchEvent ACTION_DOWN
E/MainActivity: 调用onTouchEvent第0次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_DOWN//最后到Activity的onTouchEvent,结束
E/MainActivity: 调用dispatchTouchEvent第1次//这里是抬起
E/MainActivity: dispatchTouchEvent
E/MainActivity: 调用onTouchEvent第1次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_UP
第二种情况:View消费
activityMainBinding.textTv.setOnTouchListener((v,event)->{
Log.e(TAG,"textTv --->> setOnTouchListener");
return true;
});
E/MainActivity:调用dispatchTouchEvent第0次//点击,从上往下传递
E/MainActivity:dispatchTouchEvent
E/CustomRelativeLayout:dispatchTouchEvent
E/CustomRelativeLayout:onInterceptTouchEvent
E/CustomView:dispatchTouchEvent
E/MainActivity:textTv--->>setOnTouchListener//这里返回true,就不往上返回了
E/MainActivity:调用dispatchTouchEvent第1次//这里是抬起
E/MainActivity:dispatchTouchEvent
E/CustomRelativeLayout:dispatchTouchEvent
E/CustomRelativeLayout:onInterceptTouchEvent
E/CustomView:dispatchTouchEvent
E/MainActivity:textTv--->>setOnTouchListener
第三种情况:ViewGroup拦截
CustomRelativeLayout
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
Log.e(TAG,"onInterceptTouchEvent");
return true;//注意这里,返回true,拦截事件
}
E/MainActivity: 调用dispatchTouchEvent第0次
E/MainActivity: dispatchTouchEvent
E/CustomRelativeLayout: dispatchTouchEvent
E/CustomRelativeLayout: onInterceptTouchEvent//这里返回true,拦截事件
E/CustomRelativeLayout: onTouchEvent//直接调用自己的onTouchEvent
E/CustomRelativeLayout: onTouchEvent ACTION_DOWN
E/MainActivity: 调用onTouchEvent第0次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_DOWN
E/MainActivity: 调用dispatchTouchEvent第1次
E/MainActivity: dispatchTouchEvent
E/MainActivity: 调用onTouchEvent第1次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_UP
第四种情况:ViewGroup消费
activityMainBinding.relativeLayout.setOnTouchListener((v,event)->{
Log.e(TAG,"relativeLayout --->> setOnTouchListener");
return true;
});
与情况二类似,onTouch返回true,就不再往上传递了。
关于总结:还是不借鉴各路大神的blog总结了。胖子觉得这样会造成部分朋友只看总结,不看内容,最后变成知其然不知其所以然(胖子就吃过很多这样的亏),还是交给朋友们自行总结吧。
胖子总结
- 先清楚传递Activity->ViewGroup->View和返回View->ViewGroup->Activity顺序
- 再搞清楚dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()的基本作用
- 打开源码,一步一步跟下去,自己用工具画一画它们之间的关系,之后脑袋里就会有大致概念了
- 温馨提示:点成线,线成面,切勿贪心,否则一脸懵逼
- 胖子有什么理解错误的,欢迎大家指出来,一起讨论、学习、进步
- 期待胖子的第三篇《Android事件分发(二)》