一直想写一下自己对事件分发是总结的,但是又不知道从何写起。那我们就先从一个一个案例,再到源码一步步深入总结。
案例
案例一:
btn_click = (Button) findViewById(R.id.btn_click);
btn_click.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("TAG","点击事件OnClick");
}
});
btn_click.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("TAG","点击事件OnTouch");
return false;
}
});
我们先从一个按钮的点击与触摸事件开始,我们现在来猜一下,我们点击按钮打印一个,还是会打印两个呢?
如果setOnTouchListener返回true 的时候呢?
我们可以看到,当setOnTouchListener返回true 的时候,我们的点击事件里边的语句并没有执行,返回false的时候,才会执行。这是为什么呢?
我们可以看到在View的事件分发处理dispatchTouchEvent(MotionEvent event)里边,当setOnTouchListener返回true的时候,有个result的变量值为true,如果值为true,那么在接下来的流程里边onTouchEvent(event)方法将不会执行,我们会在onTouchEvent(event)中找到这么一个方法performClickInternal(),然后再不断的往下追寻,你就会发现这样一段代码
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;
}
所以这个时候,我们可以明白当返回false的时候OnClickListener会执行打印语句,返回false的时候为什么不执行了吧。这个时候又让我想起了事件冲突的定义:事件只有一个,有多个方法想要处理。处理的对象不是我们想给的,这个时候我们就叫事件冲突。
案例二:
了解了事件冲突,那么我们接下来就开始了解事件分发了。
Activity:
public class EventDemoOneActivity extends AppCompatActivity {
private EventViewB ev_b;
private EventViewGroupA evg_a;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_event_demo_one);
initView();
}
private void initView() {
ev_b = (EventViewB) findViewById(R.id.ev_b);
evg_a = (EventViewGroupA) findViewById(R.id.evg_a);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("TAG","...........................dispatchTouchEvent..............Activity");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("TAG","...........................onTouchEvent..............Activity");
return super.onTouchEvent(event);
}
}
ViewGroup:
/**
* 版本:1.0
* 创建日期:2/23/21 10:21 AM
* 描述:
*/
public class EventViewGroupA extends LinearLayout {
public EventViewGroupA(Context context) {
super(context);
}
public EventViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("TAG","...........................dispatchTouchEvent..............EventViewGroupA");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("TAG","...........................onInterceptTouchEvent..............EventViewGroupA");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("TAG","...........................onTouchEvent..............EventViewGroupA");
return true;
}
}
View:
/**
* 版本:1.0
* 创建日期:2/23/21 10:35 AM
* 描述:
*/
public class EventViewB extends View {
public EventViewB(Context context) {
super(context);
}
public EventViewB(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("TAG","...........................dispatchTouchEvent..............ViewB");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("TAG","...........................onTouchEvent..............ViewB");
return super.onTouchEvent(event);
}
}
1.事件分发过程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三个方法协助完成
2.Android事件分发顺序:Activity(Window) -> ViewGroup -> View
3.在事件分发三大类(Activity、ViewGroup、View)中,Activity和View不会去拦截事件(也就是不能重写onInterceptTouchEvent()方法)
跑起来
我们再结合事件分发流程图来看
1.dispatchTouchEvent 和 onTouchEvent 一旦return true,事件就停止传递了(到达终点)(没有谁能再收到这个事件)。看图中只要return true事件就没再继续传下去了,对于return true我们经常说事件被消费了,消费了的意思就是事件走到这里就是终点,不会往下传,没有谁能再收到这个事件了。
2.dispatchTouchEvent 和 onTouchEvent方法在return false的时候事件都回传给父控件的onTouchEvent处理。
- 对于dispatchTouchEvent 返回 false 的含义应该是:事件停止往子View传递和分发同时开始往父控件回溯(父控件的onTouchEvent开始从下往上回传直到某个onTouchEvent return true),事件分发机制就像递归,return false 的意义就是递归停止然后开始回溯。
- 对于onTouchEvent return false 就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。
3.onInterceptTouchEvent 的作用
- onInterceptTouchEvent方法中 return true就会交给自己的onTouchEvent的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super.onInterceptTouchEvent()和return false是一样的,是不会拦截的,事件会继续往子View的dispatchTouchEvent传递。
案例三
ViewPager:
/**
* 版本:1.0
* 创建日期:5/9/21 9:35 PM
* 描述:
*/
public class BadViewPager extends ViewPager {
private int mLastX,mLastY;
public BadViewPager(@NonNull Context context) {
super(context);
}
public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
}
ListView:
/**
* 版本:1.0
* 创建日期:5/9/21 9:37 PM
* 描述:
*/
public class MyListView extends ListView {
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
Activity:
public class ListViewAndViewPagerActivity extends AppCompatActivity {
private BadViewPager bad_view_pager;
private MyPagerAdapter myPagerAdapter;
private int[] iv = new int[]{R.mipmap.iv_0, R.mipmap.iv_1, R.mipmap.iv_2,
R.mipmap.iv_3, R.mipmap.iv_4, R.mipmap.iv_5,
R.mipmap.iv_6, R.mipmap.iv_7, R.mipmap.iv_8};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_view_and_view_pager);
initView();
}
private void initView() {
bad_view_pager = (BadViewPager) findViewById(R.id.bad_view_pager);
List<Map<String, Integer>> strings = new ArrayList<>();
Map<String, Integer> map;
for (int i = 0; i < 20; i++) {
map = new HashMap<>();
map.put("key", iv[i % 9]);
strings.add(map);
}
MyPagerAdapter adapter = new MyPagerAdapter(this, strings);
bad_view_pager.setAdapter(adapter);
}
}
当ViewPager返回true 无法上下滑动,当ViewPager返回false 无法左右滑动,如果把 onInterceptTouchEvent(MotionEvent ev) 都注释掉,那么上下左右滑动正常。这是因为谷歌工程师已经给我们在源码里边进行处理了。加入谷歌工程师并没有处理,那我们应该怎么做呢?
我们先来看事件分发流程图
ViewGroup的事件分发:
在ViewGroup的 boolean dispatchTouchEvent(MotionEvent ev) 中,当为Down事件的时候,会调用cancelAndClearTouchTargets(ev); resetTouchState();对状态进行清零。
清零后,会进入如下,进行判断时间是否拦截:
如果不拦截,进行分发,就会进入如下代码,进行分发或者处理:
在代码中,我可以看到,如果是down事件,才会分发。
里边有个方法是对子View进行排序
分发判断是进行倒序排序的,并判断能否接收事件:1、VISIBLE 2、child.getAnimation()!=null 3、触点在View的范围内。
进入分发处理事件,第三个参入传入的触点处的view
如果是ViewGroup就走ViewGroup的 dispatchTouchEvent,如果是View就走View的事件分发事件。(这个属于递归事件)
如果全部不处理,就跟拦截是一样的。
如果拦截,if (!canceled && !intercepted) 将进不去,直接走到下边(分发或者处理),事件到底处不处理,相当于是最后一个:
首先,第一次mFirstTouchTarget肯定为空,dispatchTransformedTouchEvent这个方法就是处理;
这个时候是最后一个,子View为空 。
所以,分发的最终又回到了View 的 dispatchTouchEvent(MotionEvent event),也就是回到了我们前边的案例一事件处理中
所以,当Viewpager拦截的时候,ListView是无法处理事件,所以没办法上下滑动。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
为true 就没办法事件分发。
我们再来看看move事件,move不会再走分发
总结:
ViewGroup需要先走分发流程,如果没人处理,就再走处理流程;View只能走处理流程
分发流程:1、先看是否拦截后自己处理 2、分发下去:排序、遍历分发、领取View的处理事件。3、没人处理,再看下自己是否处理事件;
move事件不会再走分发事件
那么我们应该怎么处理事件冲突?
1、内部拦截法,子View处理事件分发
View:
/**
* 版本:1.0
* 创建日期:5/9/21 9:37 PM
* 描述:
*/
public class MyListView extends ListView {
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 内部拦截法:子view处理事件冲突
private int mLastX, mLastY;
@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:(当它为Down事件的时候不拦截)
public class BadViewPager extends ViewPager {
private int mLastX,mLastY;
public BadViewPager(@NonNull Context context) {
super(context);
}
public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction()==MotionEvent.ACTION_DOWN){
super.onInterceptTouchEvent(ev);
return false;
}
return true;
}
}
2、外部拦截法,是父容器处理冲突
Listview:
/**
* 版本:1.0
* 创建日期:5/9/21 9:37 PM
* 描述:
*/
public class MyListView extends ListView {
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
ViewPager:
/**
* 版本:1.0
* 创建日期:5/9/21 9:35 PM
* 描述:
*/
public class BadViewPager extends ViewPager {
private int mLastX,mLastY;
public BadViewPager(@NonNull Context context) {
super(context);
}
public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@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);
}
}