自定义View从实现到原理(三)
上一篇博客我们讲到,在我们点击屏幕时,就会产生点击事件,这些点击事件被封装为了一个类:MotionEvent,而这个时候,我们的点击事件会先传递给当前的Activity,所以我们也有必要了解一下Activity的组成,详细的可以看我上一篇博客:
自定义View从实现到原理(二)- 源码解析Activity的构成
那么现在我们就正式进入View的事件分发机制。
源码解析View的事件分发机制
View的事件分发机制
点击事件发生之后,首先传递给Activity,这回就会调用Activity的dispatchTouchEvent()方法,根据我们上一篇所讲,这些具体的事件处理顺序自然就是:Activity中的PhoneWindow — PhoneWindow中的DecorView — DecorView的ViewGroup这个顺序。了解了这些之后,我们直接看最后一步,也就是ViewGroup的dispatchTouchEvent()方法,主要的部分代码:
/**
* 将触摸屏事件向下传递到目标视图,或者当前视图
* 如果现在就是目标的话。
*
* @param ev 要向下分派的运动事件
* @return 如果事件由视图处理,则为True,否则为false。
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
****
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget !=null) {//1
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//2
if(!disallopwIntercept){
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
}else{
intercepted = false;
}
}else{
intercepted = true;
}
****
return handled;
}
首先进行了判断,判断点击事件是否为DOWN事件,如果是的话那么下面这两个函数将会进行初始化工作,resetTouchState()方法中会把下面进行判断的mFirstTouchTarget的值置为null。
为什么要进行初始化?
因为一个完整的点击事件序列是以DOWN开始,UP结束的,因此如果是DOWN事件开始的话,那么就证明是一个新的点击事件,因此需要进行初始化。
我们接着看下面部分,看标注1的代码部分,这一部分的判断中,mFirstTouchTarget代表的就是当前的ViewGroup是否对点击事件进行了拦截,拦截了的话mFirstTouchTarget就会是null; 如果没有拦截则会交由子View进行处理,这个时候的mFirstTouchTarget !=null。我们假设当前的ViewGroup拦截了此点击事件:
此时mFirstTouchTarget =null,如果这是触发了ACTION_DOWN我们就会进入第一个if,也就是标有2处的代码部分,这个时候我们就会执行onInterceptTouchEvent(ev)方法,那么相对触发的是ACTION_UP以及ACTION_MOVE时则就不会触发onInterceptTouchEvent(ev)方法,而是直接将intercepted置为true,此后的一个事件序列均由这个ViewGroup进行处理。再看一下注释2的位置那一串代码,出现了一个FLAG_DISALLOW_INTERCEPT标志位,作用主要是制止ViewGroup拦截除了DOWN以外的事件。
这一部分我的总结就是:ViewGroup进行事件拦截的时候,这个事件的后续序列都会交给它来处理(ACTION_UP以及ACTION_MOVE),这个时候就无需调用onInterceptTouchEvent(ev)这个方法,所以我们知道,这个方法事件不是每次都会被调用的,接下来我们看一下这个方法的代码:
public boolean onInterceptTouchEvent(MotionEvent ev){
return false;
}
我们可以看到这个方法返回值是false,意味着不会进行事件拦截。由此我们就可以知道,如果我们自定义的ViewGroup想要拦截事件时,有必要重写这个方法。
继续看上面dispatchTouchEvent()方法的其他重要部分代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
****
for(int i = childrenCount - 1; i >= 0; i--){ //1
****
if(!canViewRecrivePointerEvents(child)
|| !isTransformedTouchPointInView(x,y,child,null)){ //2
ev.setTargetAccessibilityFocus(false);
continue;
}
****
if(dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)){ //3
****
}
****
}
****
}
我们来梳理一下上面的几行关键代码:在1处的是一个for循环,这个循环遍历的是ViewGroup的子元素,也就是下面的ViewGroup以及子View,根据接下来的判断来决定子元素是否能够接收到点击事件,如果有子元素可以接收的话,那么就交给子元素进行处理这个事件。我们看一下这个遍历,i–,这个遍历是倒序遍历,即从ViewGroup的最上层子View开始进行处理判断;
在2处的是一个if判断,判断的是这个点击事件的触摸点是否在子View的范围内或者这个子View是否是在进行播放动画,如果都不符合的话,那么就执行continue语句来表示这个子View并不符合要求,进行遍历下一个子View;(代码中写的是!,但是读的时候还是要按照正常想法阅读,就这样:如果点击事件的触摸点不在子View的范围内或者这个子View没有在进行播放动画,那么这个View排除);
在3处的是另一个判断,我们来看一下这个dispatchTransformedTouchEvent()方法的代码:
private boolean dispatchTransformedTouchEvent(MotionEvent event , boolean cancel ,
View child , int desiredPointerIdBits){
final int oldAction = event.getAction();
if(cancel || oldAction = MotionEvent.ACTION_CANCEL){
if(child == null){
handled = super.dispatchTouchEvent(event);
}else{
handled = child.dispatchTouchEvent(event);
}
****
return handled;
}
****
}
这一部分很好理解,判断是否有子View,如果有的话调用子View的dispatchTouchEvent()方法,如果没有的话就调用super.dispatchTouchEvent()方法,由于ViewGroup也是继承自View的,那么我们去看一下这个super的dispatchTouchEvent()的方法,即View的方法:
public boolean dispatchTouchEvent(MotionEvent event){
****
boolean result = false;
if(onFilterTouchEventForSecurity(event)){
ListenerInfo li = mListenerInfo;
if(li !=null && li.mOnTouchListener != null
****
&& li.mOnTouchListener.OnTouch(this,event)){
result = true;
}
if(!result && onTouchEvent(Event)){
result = true;
}
}
****
return result;
}
如果OnTouchListener不为null,而且onTouch方法返回true,那么表示的就是事件正在被消费(我理解的就是处理),这个时候我们就不会执行OnTouchEvent(event)方法,否则就会执行。接下来我们就看一下onTouchEvent(event)方法的代码:
public boolean onTouchEvent(MotionEvent event){
****
final int action = event,getAction();
if(((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)}{
switch(action){
case MotionEvent.ACTION_UP:
****
performClick();
****
}
return true;
}
return false;
}
在上面的判断中我们可以看出,只要View的CLICKABLE或者LONG_CLICKABLE中有一个是true的话,那么onTouch就会返回true来消耗接受处理这个事件,上面这两个我们自然都不会陌生,在View中设置setOnClickListenter以及setOnLongClickListener即可设置,他们会自动设置为上面那两个----CLICKABLE以及LONG_CLICKABLE,接着在ACTION_UP这个事件中会调用performClick()这个方法:
public boolean performClick(){
****
if(li != null && li.mOnClickListener != null){
****
li.mOnClickListener.onClick(this);
return true;
}
****
}
截取了很简单的一部分,就是一个判断,这个判断的意思就是,如果View设置了点击事件OnClickListener,那么它设置的onClick方法就会进行执行。
到这里View的事件分发机制源码分析基本就到这里了,是不是一头雾水,别急,接下来我们来梳理一下点击事件分发的传递规则:
点击事件分发的传递规则
简单的来说,点击事件的MotionEvent在View中的层级传递就是点击事件分发,根据上面所看的源码,我们主要可以总结出三个重要的方法,点击事件中有这三个方法:
方法 | 作用 |
---|---|
dispatchTouchEvent(MotionEvent ev) | 用来进行事件的分发 |
onInterceptTouchEvent(MotionEvent ev) | 用来进行事件拦截,在dispatchTouchEvent(MotionEvent ev)中调用,只有ViewGroup中存在,View中并没有 |
onTouchEvent(MotionEvent ev) | 用来处理点击事件,在dispatchTouchEvent(MotionEvent ev)中调用 |
onInterceptTouchEvent(MotionEvent ev) 以及 onTouchEvent(MotionEvent ev) 都会在dispatchTouchEvent(MotionEvent ev)这个方法中进行调用,我们来梳理一下点击事件分发的传递规则:
1.点击事件产生后被封装为MotionEvent类交予Activity进行处理;
2.Activity交予PhoneWindow,交予DecorView,交予顶层的ViewGroup;
3.对于根ViewGroup,点击事件首先传递给他的dispatchTouchEvent(MotionEvent ev) 方法;
4.调用onInterceptTouchEvent(MotionEvent ev)方法;
5.1:返回true,证明要拦截这个事件,那么这个事件就会交给他的onTouchEvent()方法处理;
5.2:返回false,表示不拦截,那么就会判断是否有子元素;
5.2.1:如果有的话就会交给子元素的dispatchTouchEvent(MotionEvent ev),紧接着从3开始;
5.2.2:如果没有的话就会调用super的dispatchTouchEvent(MotionEvent ev)方法,通常最终会调用View的dispatchTouchEvent(MotionEvent ev),一般来说最终就会调用View的onTouchEvent(MotionEvent ev)方法。
6.在底层View的onTouchEvent()方法返回值是true时,
则会执行底层View的onTouchEvent()命令,
如果返回值时false的话,就证明并没有完成,所以result会被置为false,
接下来这样的话底层View的dispatchTouchEvent()方法也会返回false,
然后父View的dispatchTouchEvent()方法的handled会因为View的dispatchTouchEvent()方法返回false从 而被置为false,
这样父View的则会调用onTouchEvent()方法,以此类推,直到返回true;