分发流程
安卓的事件分发流程,并没有网上说的那么复杂
之所以网上很多讲得非常复杂,是因为动不动就拿系统源码说事
其实系统源码大多都是繁杂的细节处理,真正核心的东西剥离出来,并没有多少
还有就是,有的博主把流程图做得太细太复杂了
其实只要把最核心的流程讲清楚,剩下的让读者自己去思考发散下,就已经够了
图画得太细,反而让很多人根本看不下去
这里一张图,已经足够总结了整个事件分发的完整流程了
核心知识点
上面的图,基本上已经包含了全部的知识,这里再以文字的形式,列举其中最核心的内容
这是因为,整张图看下去,很容易忘,文字印象会更深刻点,图适合用于理解整个流程
所以这里既给了文字版,也给了图形版
- dispatch方法,用于将父View的事件分发给子View
- intercept方法,用于拦截当前事件,不分发给子View
- onTouchEvent和OnTouchListener,用于对事件进行处理
- 不管是dispatch方法,还是onTouch方法,true表示事件已处理,false表示未处理
- 父View是通过子View的onDispatchTouchEvent方法的返回值,来判断子View有没有处理事件的
- OnTouchListener和onTouchEvent,都只是onDispatchTouchEvent的内部逻辑而已,父View是不管这些的
- 默认情况下,子控件onTouchEvent的返回值,就是子控件onDispatchTouchEvent方法的返回值,但是我们可以通过重写来打破默认逻辑
- 如果onTouchEvent返回了false,onDispatchTouchEvent返回了true,对父控件而言,子控件处理了这个事件,父控件只看dispatch方法返回值
- 当一个子控件处理了TouchEvent,余下的同级子控件,就收不到TouchEvent了
- 当所有子控件都未对事件进行处理时,父控件将调用View基类中的onDispatchTouchEvent方法来处理事件
- View.onDispatchTouchEvent调用OnTouchListener监听器和onTouchEvent方法来处理事件,优先使用监听器来处理事件,当监听器不存在,或监听器未处理时,才调用onTouchEvent来处理,监听器和onTouchEvent的处理结果,就是View.onDispatchTouchEvent的返回值
- 重写分发流程,一般主要工作是修改方法返回值,但要记得调用super方法,因为事件分发是在super方法中实现的,如果不调用,事件就不会向下分发了
下沉传递和冒泡响应
读懂了上面的流程后,我们可以发现
整个事件流程,是一个先从上而下传递,再从下而上处理的U型过程
这个过程在业内,有个专业的术语,叫做下沉传递和冒泡响应
dispatch是由Activity传给ViewGroup,再传给View,如果其中有一个节点对分发流程进行了intercept,则停止继续分发
onTouchEvent是由View到ViewGroup,再到Activity,如果其中有一个节点对事件进行了处理,则停止向上冒泡,上级不需要再处理
测试方案
想要把事件分发流程吃透,光靠看是不行的,一定要自己多动手验证下,才能印象深刻
验证方法很简单,我们自定义两个View,两个ViewGroup
重写View,ViewGroup,Activity中和事件分发相关的方法
通过修改返回值,我们就能验证实际效果和理论是不是一致的了
package com.android.architecture.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.easing.commons.android.code.Console;
@SuppressWarnings("all")
public class V1 extends View {
public V1(Context context) {
this(context, null);
}
public V1(Context context, AttributeSet attributeSet) {
this(context, attributeSet, 0);
}
public V1(Context context, AttributeSet attributeSet, int style) {
super(context, attributeSet, style);
init(context, attributeSet);
}
protected void init(Context context, AttributeSet attributeSet) {
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
Console.info("View1 dispatchTouchEvent ACTION_DOWN");
super.dispatchTouchEvent(event);
return true;
}
if (event.getAction() == MotionEvent.ACTION_UP)
Console.info("View1 dispatchTouchEvent ACTION_UP");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN)
Console.info("View1 onTouchEvent ACTION_DOWN");
if (event.getAction() == MotionEvent.ACTION_UP)
Console.info("View1 onTouchEvent ACTION_UP");
return false;
}
}
package com.android.architecture.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import com.easing.commons.android.code.Console;
@SuppressWarnings("all")
public class G1 extends FrameLayout {
public G1(Context context) {
this(context, null);
}
public G1(Context context, AttributeSet attributeSet) {
this(context, attributeSet, 0);
}
public G1(Context context, AttributeSet attributeSet, int style) {
super(context, attributeSet, style);
init(context, attributeSet);
}
protected void init(Context context, AttributeSet attributeSet) {
setClickable(true);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN)
Console.info("G1 onInterceptTouchEvent");
return false;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
Console.info("G1 dispatchTouchEvent ACTION_DOWN");
super.dispatchTouchEvent(event);
return true;
}
if (event.getAction() == MotionEvent.ACTION_UP)
Console.info("G1 dispatchTouchEvent ACTION_UP");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN)
Console.info("G1 onTouchEvent ACTION_DOWN");
if (event.getAction() == MotionEvent.ACTION_UP)
Console.info("G1 onTouchEvent ACTION_UP");
return false;
}
}
package com.android.architecture;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import com.easing.commons.android.app.CommonActivity;
import com.easing.commons.android.code.Console;
import com.easing.commons.android.ui.control.title_bar.TitleBar;
import com.easing.commons.android.ui.dialog.BottomSlideDialog;
import butterknife.BindView;
import lombok.SneakyThrows;
@SuppressWarnings("all")
public class StartActivity extends CommonActivity {
@BindView(R.id.titleBar)
TitleBar titleBar;
@BindView(R.id.container)
FrameLayout container;
BottomSlideDialog dialog;
@Override
protected boolean beforeCreate() {
statusBarColor = R.color.color_transparent;
navigationBarColor = R.color.color_light_blue;
return super.beforeCreate();
}
@Override
@SneakyThrows
protected void create() {
setContentView(R.layout.activity_start);
titleBar.showReturnButton(false);
titleBar.setBackgroundResource(R.drawable.color_light_blue);
titleBar.setTitleText("Android");
dialog = BottomSlideDialog.create(ctx, R.layout.dialog_menu);
init();
}
@Override
public void onBackPressed() {
dialog.show();
}
protected void init() {
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN)
Console.info("Activity dispatchTouchEvent ACTION_DOWN");
if (event.getAction() == MotionEvent.ACTION_UP)
Console.info("Activity dispatchTouchEvent ACTION_UP");
super.dispatchTouchEvent(event);
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN)
Console.info("Activity onTouchEvent ACTION_DOWN");
if (event.getAction() == MotionEvent.ACTION_UP)
Console.info("Activity onTouchEvent ACTION_UP");
return false;
}
}
ACTION_UP和ACTION_DOWN之间的关联
按下和弹起事件,一定是成对出现的,操作系统会对事件做一致性校验
当dispatchTouchEvent接收到了Down事件时,只有返回了true,下次才能接收到与之对应的Up事件
如果dispatchTouchEvent在处理Down事件时返回了false,则下次的Up事件,不会再分发给该控件
同理,ACTION_MOVE和ACTION_DOWN的关系也是如此
onClick和onTouchEvent之间的关联
onClick是onTouchEvent方法中的一个子处理
当onTouchEvent触发了Down事件,又触发了Up事件,中间触摸区域没有离开过控件范围
便会被视为一次点击事件,然后onTouchEvent会执行performClickInternal代码,来完成点击事件的处理
当然,这是View的默认行为,如果我们重写了onTouchEvent等方法,或没有调用super方法
那么onClick就不会执行,这就是为什么很多人重写触摸事件之后,点击事件失效的原因