一、背景
在Android开发过程,不管你乐意与否一定会接触到自定义View,而自定View的时候一定会遇到各种各样的坑等着你。而滑动冲突就是其中的一个坑。而填坑的解决方案就在源码中。所以理清楚事件分发的逻辑至关重要。
二、事件分发
1、事件分发的对象是谁
答案:点击事件(Touch
事件)
而Touch事件又分为如下四种
事件 | 简介 |
ACTION_DOWN | 手指 初次接触到屏幕 时触发 |
ACTION_MOVE | 手指 在屏幕上滑动时触发,会多次触发 |
ACTION_UP | 手指 离开屏幕 时触发 |
ACTION_CANCEL | 事件 被上层拦截 时触发 |
一般情况下事件都是以DOWN事件开始、UP事件结束,中间伴随着无数的MOVE事件,如下图:
事件在传递的过程会经历三层如下图:
这个过程中,事件每经过一层都会触发一个最关键的核心方法:
- dispatchTouchEvent()
2、Activity事件分发
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {//事件分发都是从用户按下那一刻,产生DOWN事件开始的
onUserInteraction();//一般都是用来做屏保功能的,但是平时开发中我们基本不会重写该方法
}
if (getWindow().superDispatchTouchEvent(ev)) {//将事件传递给PhoneWindow,再有PhoneWindow通过DecodView分发给ViewGroup
//如果Activity下的控件消费了事件则直接返回,停止事件传递
return true;
}
return onTouchEvent(ev);//没有控件消费事件则直接调用onTouchEvent
}
3、ViewGroup事件分发
在ViewGroup的disPatchTouchEvent中首先会做如下逻辑判断:
- 是Down事件:
- 拦截事件:执行ViewGroup自身的supper.dispatchTouchEvent()方法来执行事件处理
- 不拦截事件:向下分发,根据Event事件的坐标信息,判断点击是否在子View的范围内,在子View的范围内分发给子View。如果子View消费了事件(返回true),则记录消费事件的子View(firstTargetTouch)。
- 非Down事件:
- 之前的事件 有子View消费(firstTargetTouch != null):判断是否拦截事件,如果拦截执行ViewGroup自身的supper.dispatchTouchEvent()方法来执行事件处理,如果不拦截则直接分发给firstTargetTouch处理。
- 之前的事件无子View消费(firstTargetTouch == null):直接拦截, 执行ViewGroup自身的supper.dispatchTouchEvent()方法来执行事件处理
需要注意的点是这里说的子View本身可能也是一个ViewGroup。这就是著名的责任链模式。
3、View事件处理
ViewGroup 将事件传递到子View后,如果子View不是ViewGroup就会执行子View的事件处理逻辑。所以View 的 dispatchTouchEvent()方法同样是我们需要关注的重点
红框中的三个条件,第一个我就不用说了。
- (mViewFlags & ENABLED_MASK) == ENABLED
该条件是判断当前点击的控件是否为 enable,但由于基本 View 都是 enable 的,所以这个条件基本都返回 true。而handleScrollBarDragging()则是判断当前View是否正在处理拖拽。
- mOnTouchListener.onTouch(this, event)
即我们调用 setOnTouchListener() 时必须覆盖的方法 onTouch() 的返回值。
- 当上面两个判断执行后只要事件没有被消费,即result依旧为false。就会继续调用该View自身的onTouchEvent()方法。
所以OnTouchListener的onTouch() 方法优先级高于View自身的 onTouchEvent(event) 方法。
三、事件拦截
都说事件拦截是ViewGroup独有的特性,因为它有一个抽象的onInterceptTouchEvent()方法供开发者去实现拦截功能。但实际上事件拦截是分为两种的:父控件拦截、子控件拦截。这里我们就简单的说一下这两种拦截的方式。
1、父控件拦截
既然是父控件,那必然是ViewGroup了。所以重写onInterceptTouchEvent来实现拦截是必然的了。那么我们就跟着源码分析一下,重写这个onInterceptTouchEvent方法后,是如何实现拦截的?并且是否只要拦截一个事件其他事件同样不会往下传递了呢?
2、子控件拦截
子控件拦截说白了就是请求父控件不要拦截事件,将事件分发到子View,由子View来消费该事件。
当子View想要获取事件,但是事件又被父控件把持着不下发的时候就需要子View调用ViewGroup的requestDisallowInterceptTouchEvent(true)来请求事件(实际上就是设置一下mGroupFlags这个标志位的值)。但是子View调用了这个方法就必然能够获取到事件吗?比如父控件的onInterceptTouchEvent()方法直接返回true的情况下。是否能够通过这种方法请求到事件下发呢?答案是不能!答案就在每次Down事件时,ViewGroup都会重置这个标志位,这就导致了子View发起的请求无效了。
所以子控件拦截的前提条件就是父控件不能拦截Down事件,父控件只会拦截除了Down事件之外的事件,而Down事件继续分发到子View。子View接收到Down事件后再调用requestDisallowInterceptTouchEvent(true)方法请求其他事件的下发。如果子View不需要某个事件了再调用requestDisallowInterceptTouchEvent(false)将事件拦截的权利重新交给父控件。
实际上我们在后续解决滑动冲突的时候遵循的就是这两种方式。
四、事件消费
我们都知道ViewGroup实质上也是继承自View,它同样有着View的属性。而在事件分发过程中,不管该事件是继续向子View下发交给子View处理还是拦截下来自己处理,最终都会执行View的dispatchTouchEvent()方法。
而事件的消费就从View的dispatchTouchEvent()开始。
public boolean onTouchEvent(MotionEvent event) {
.......
// 若该控件可点击,则进入switch判断中
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
// a. 若当前的事件 = 抬起View(主要分析)
case MotionEvent.ACTION_UP:
...// 经过种种判断,此处省略
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// 执行performClickInternal() ->>分析1
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
// b. 若当前的事件 = 按下View
case MotionEvent.ACTION_DOWN:
....
if (!clickable) {
//执行长按事件
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
// c. 若当前的事件 = 结束事件(非人为原因)
case MotionEvent.ACTION_CANCEL:
......
break;
// d. 若当前的事件 = 滑动View
case MotionEvent.ACTION_MOVE:
......
break;
}
// 若该控件可点击,就一定返回true
return true;
}
// 若该控件不可点击,就一定返回false
return false;
}
分析一
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
//分析二:执行performClick
return performClick();
}
分析二
public boolean performClick() {
...
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// 只要我们通过setOnClickListener()为控件View注册1个点击事件
// 那么就会给mOnClickListener变量赋值(即不为空)
// 则会往下回调onClick() & performClick()返回true
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
.....
return result;
}
当dispatchTouchEventt返回true即表示View消费了该事件,他的父View会记录消费了事件的子View。