View的集成关系树:
ViewGroup继承View,用来包含显示View。View的子类是一个矩形用来显示图案,也称为Widget。
View的事件分发
View的事件基础分析
View的onClick事件分析
例子说明:我们给一个View设置两个事件,一个是OnClickListener,另外一个是onTouchListener。
1 | findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() { |
在点击该View的时候
- 当onTouch返回true的时候,OnClick将不会有响应
- 当onTouch返回false的时候,二者都有响应。
*为什么会这样子的呢?*黑人问好❓
我们需要知道onClick事件的流程,以及他在哪里被调用了。
View的点击事件响应流程:
- 在setOnClickListener的时候做的事情是设置可点击,以及把改Listener存储到ListenerInfo这个List中。对于设置OnTouchLdiistener的时候,同样的,主要的代码如下:这是设置click和Touch事件的时候,会把OnClickListener和OnTouchListener赋值进去。
1
2
3
4
5
6
7
8
9
10
11// Click事件
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
// onTouch事件
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
} - 寻找自己的dispatchTouchEvent方法,自己寻找不到,寻找父类的。(事件都是先去执行该方法,后面说到)
在TextView中没有dispatchTouchEvent方法,所有去寻找父类的,对于TextView该方法在View类中,主要的代码是:使用的是API23的源码,其中reslut就是dispatchTouchEvent()返回的值。当我们设置了OnTouchListener的时候,OnTouchListener的 onTouch(View view, MotionEvent motionEvent) 返回的是true的时,就不会去执行View的 onTouchEvent(MotionEvent event)事件了。也就是第二行的条件使得result为true了,那么第七行的onTouchEvent(event)方法就不会被执行了,而我们的OnClickListener就是在onTouchEvent(MotionEvent event)中执行的。1
2
3
4
5
6
7
8
9
10
11
12
13
14public boolean dispatchTouchEvent(MotionEvent event) {
// 省略代码...
ListenerInfo li = mListenerInfo; //第1行
if (li != null && li.mOnTouchListener != null //第2行
&& (mViewFlags & ENABLED_MASK) == ENABLED //第3行
&& li.mOnTouchListener.onTouch(this, event)) { //第4行
result = true; //第5行
} //第6行
if (!result && onTouchEvent(event)) { //第7行
result = true; //第8行
} //第9行
// ... 省略代码
return result; //第10行
}
在View的onTouch方法中,在action为ACTION_UP的时候,有这样子的代码:其中performClick()是:1
2
3if (!post(mPerformClick)) {
performClick();
}所以,假如是在onTouchEvent(event)中的ACTION_UP返回了true,onClick事件就不会被调用了。1
2
3
4
5
6
7
8
9
10
11
12
13public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this); //执行onClick事件
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
流程图是:
View的onTouch(view,event) 事件说明:
首先是需要知道有两个onTouchEvent(event)的,一个是View的,一个是Activity的,优先级是Activity的比较高一点,对于View而言,他还可以设置onTouchListener,里面有一个onTouch(view,event),他的优先级比View的onTouchListener高。后面会说到。
在View的onTouchEvent中,假如他被调用了(view的onTouch(view,event)没有返回true),流程有一个判断是:
1 | public boolean onTouchEvent(MotionEvent event) { |
当这个条件是true的时候,onTouchEvent会返回true,无论是哪一个ACTION(DOWN,UP,MOVE)都会返回true,使得方法onTouchEvent返回true。
在View设置了onClickListener事件的时候,假如该View是不可以点击的,就会激活它(看上面的setOnClickListener()方法),调用setFlag方法,使得上面这个条件为true,View的onTouchEvent可以返回true。**disPatchTouchEvent只有在返回了true的时候,才会继续分发事件(后面会在提到)**。所以结论是:
- 在View没有设置onTouchListener的时候,会执行onTouchEvent(event)方法,会执行那几次,需要看返回值。(DOWN,MOVE,UP)的时候的返回值,在返回了false之后不会再执行之后的事件,只有DOWN是一定会执行的。DOWN返回了false之后就不在执行,对于其他的类型。当直接返回super.onTouchEvent(event)作为返回值的时候,需要看继承的View,比如TextView的就会在DOWN之后就返回了false,那么默认就是只会执行DOWN。对于Button而已却是返回了true,事件可以传递下去,所以MOVE和UP都可以执行。
- 在设置了onTouchEvent的时候,onTouch(view,event)返回了false就会执行onTouchEvent(event),假如这时候onTouchEvent(event)返回了true,那么事件就可以继续分发。假如返回了false,那么事件就会终止。假如返回了onTouch(view,event)true就不会执行。(看上面的dispatchEvent(event)方法)。
流程图是:
View事件的深入了解
View的事件涉及到几个方法,dispatchTouchEvent,onInterceptTouchEvent(只有ViewGroup以及他的子类才会有该方法)以及onTouchEvent,他们都会返回一个boolean值。下面是一些拥有改方法的一些比较:
|方法|描述|Activity|ViewGroup|View|
| :–: || :–:| :–:| :–:| :–:|
| dispatchTouchEvent |事件分发|有|有|有|
| onInterceptTouchEvent |事件拦截|无|有|无|
| onTouchEvent |事件消费|有|有|有|
事件的传递大概顺序是:Activity-ViewGropu-View
其实完整的事件传递是:
1 | WMS -> ViewRootImp -> PhoneWindow$decorView -> Activity -> PhoneWindow->PhoneWindow$decorView -> ViewGroup -> -> View(ViewGroup) |
为什么从Activity开始解析
ActivityThread在handlerResume调用Activity的performResume(),之后再去通过Instrumentation调用Activity的onResume()方法,之后再调用Activity的makeVisible()方法,显示DecorView。
makeVisible()代码如下:
1 | void makeVisible() { |
其中,在WindowManager的addView()中,通过WindowManager的实现类,WindowManagerImpl,调用了WindowManagerGlobal的addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) 方法,代码如下:
1 | @Override |
mGlobal的addView方法会调用实例化一个ViewRootImpl,并且调用他的setView方法,重要代码如下
1 | public void addView(View view, ViewGroup.LayoutParams params, |
该setView方法通过跨进程调用WMS(WindowManagerService),然后把WMS,Activity,DecorView,Window绑定起来。通过WMS接受硬件的输入事件,传递给ViewRootImpl,然后ViewRootImpl中通过调用不同的事件舞台(InputStage)处理相关事件,对于点击事件是ViewPostImeInputStage,在他的processPointerEvent()方法中调用如下:
1 | private int processPointerEvent(QueuedInputEvent q) { |
其中,这一个View当初的setView方法赋值进来的,也就是DecorView他没有实现View的processPointerEvent(q),View中的实现如下:
1 | public final boolean dispatchPointerEvent(MotionEvent event) { |
假如是点击触摸事件,则会调用dispatchTouchEvent(event),也就是DecorView的dispatchTouchEvent(event)方法,**注意的是他并没有去super在ViewGroup的dispatchTouchEvent()**,在DecorView中实现是:
1 | @Override |
这里的cb对象就是我们的Activity了。
怎么说吗cb就是Activity呢?首先在Activity中,实现了Window.Callback接口,然后在Activity中 attach()方法实例化Window的唯一子类PhoneWindow,并且把Activity的引用传递进去。代码如下:
1 | final void attach(Context context, ActivityThread aThread, |
交给了Activity之后,在Activity的dispatchTouchEvent中会调用PhoneWindow的superDispatchTouchEvent()
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
然后再在PhoneWindow的superDispatchTouchEvent(ev)中调用DecorView的superDispatchTouchEvent,之后就会到DecorView的super.dispatchTouchEvent()(ViewGroup中)进行分发。
这样子的就知道是为什么从我们的Activity开始分发事件了。整个流程是:WMS收到硬件的touch事件,传递给ViewRootImpl,ViewRootImpl调用DecorView的dispatchPointerEvent(父类ViewGroup中),然后dispatchPointerEvent再调用DecorView的dispatchTouchEvent(),最后调用了Activity的dispatchTouchEvent(),然后调用PhoneWindow,然后再去调用DecorView的super.dispatchTouchEvent(event)进行分发。
可以看一下这边文章Android中MotionEvent的来源和ViewRootImpl
事件分发逻辑流程
从上面的分析,我们可以可以接触到的其实是Activity的,ViewGroup的,View的事件分发,像PhoneWindow,DecorView,ViewRootImpl的事件分发一般不用我们处理(但是知道整个流程对于我们的理解有好处)。所以我们就分析一下Activity,ViewGroup,View这三者的事件。
例子代码:
Activity的
1 | public class MainActivity extends Activity { |
ViewGroup的:
1 | public class CustomViewGroup extends LinearLayout { |
View的:
1 | public class CustomView extends TextView { |
布局文件:
1 | <?xml version="1.0" encoding="utf-8"?> |
运行之后直接点击Hello World结果是:
1 | Activity dispatchTouchEvent:0 |
我们所有的方法事件都是处理为super,暂时不返回true或者是false,可以看到按照这个事件流程,在ACTION_DOWN的时候,Activity先执行dispatchTouchEvent开始分发事件,然后按照上面Activity的dispatchTouchEvent()方法逻辑,他会去调用DecorView的dispatchTouchEvent()方法进行分发,然后一路下去,给CustomViewGroup, CustomViewGroup再去调用子View的dispatchTouchEvent,然后是到子View的onTouchEvent()一路返回回来,但是在ACTION_UP的时候,我们的CustomViewGroup和CustomView就没有分发到UP事件,这是为什么呢?难道我们的ViewGroup无法接受到UP事件?后面我们再提这个,我们来分析一下这次log的流程,按照事件打印的顺序,DOWN事件是很明显符合我们上面为什么从Activity开始分发的逻辑的:
本文是使用 API23的源码进行分析和debug的。
Activity的dispatchTouchEvent
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
看Activity的该方法源码我们知道
- 假如我们什么都不做,他将会执行Activity的onTouchEvent,或者是无卵返回了其他的东西,只要去super了父类,他都会执行自身的onTouchEvent方法。
- 假如重写了该方法,但是我们没有手动调用super.dispatchTouchEvent(event)或者是调用onTouchEvent(event),那么事件将终止传递,相当于消费了touch事件。
- 假如调用的getWindow().superDispatchTouchEvent(ev)返回了true,那么就不会去调用Activity的onTouchEvent(v)了。
所以一般情况下我们没有必要重写该方法,因为一不小心可能我们的其他任何时间都无法接受到了。
接下来来分析假如去super了该方法的时候发生什么事情,在没有重写或者是super了该方法的时候,我们从为什么从Activity开始分发事件分析知道,他将会去调用DecorView父类(ViewGroup)的dispatchTouchEvent事件。
ViewGroup的dispatchTouchEvent(event)
主要涉及代码:
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
代码比较多,大家可以看注释的部分,注意看到不是ACTION_DOWN的时候,而且默认子View一直是返回了false(后面分析),默认是拦截掉事件的,这也就解析了上面打印的Log,为什么在ViewGroup中没有看到ACTION_UP的事件,因为在PhoneWindow调用ViewGroup的时候,由于action是UP,主动消费拦截了事件,当前子View(CustomViewGroup)返回了false,没有添加到父ViewTouchTarget中,也就没有给子View传递(详细看后面的一点疑问)。除此之外,我们看到当onInterceptTouchEvent方法方法返回了false,也是会去分发事件的(此时的应该是ACTION_DOWN),他的代码是:
1 | public boolean onInterceptTouchEvent(MotionEvent ev) { |
可以看到他是默认没有拦截事件的,默认返回了false。只有没有拦截事件,ViewGroup的dispatchTransformedTouchEvent()方法,他的主要代码是:
1 | //只要有子view或者是View处理了事件的分发,就返回handled; |
可以看到,该返回的返回值是看View中的dispatchTouchEvent或者是子view的dispatchTouchEvent的返回值。ViewGroup本身去super调用是在mFirstTouchTarget对象为空或者是传入上面的child对象为空的时候被调用,也就是ViewGroup的onTouchEvent是在子View的dispatchTouchEvent和onTouchEvent之后的。
分析总结:
- 在我们重写了该方法,而没有去super父类的dispatchTouchEvent(),事件终止传递。
- 在我们有super该调用方法的时候,他将会在最后调用父类容器的dispatchTouchEvent,同时之前会去进行事件的分发。有以下情况:
- 首先他肯定回去调用onInterceptTouchEvent()方法,假如onInterceptTouchEvent返回了true,则是onInterceptTouchEvent消费了该事件,终止事件传递,,默认是返回false不消费的,向下传递。
- ViewGroup的onInterceptTouchEvent没有拦截事件,则会去遍历子view,调用子view的dispatchTouchEvent()。
- 在mFirstTouchTarget为空或者是子View的dispatchTouch()返回了false的时候,回去调用父类的dispatchTouchEvent方法,其实是通过dispatchTransformedTouchEvent()去调用。
接下来我们讨论View的dispatchTouchEvent();
ViewGroup的onInterceptTouchEvent(event)
该方法的源码比较简单,默认是返回false的,上面也有分析到,当放回false的时候,他是向下继续传递,当返回了true的时候,那么他就会把事件交给自己的onTouchEvent()去处理了。
View的dispatchTouchEvent(event)
他的大概源码是
1 | public boolean dispatchTouchEvent(MotionEvent event) { |
注释写大概写明白了,注意的是View的是没有onInterceptTouchEvent的,因为他是最小的组件,不能包含其他组件,所以不能分发事件。
可以看到在我们没有手动给View设置onTouchListener或者是返回了false的时候,才会去执行onTouchEvent()方法,然后假如onTouchEvent()返回了false则是View的dispatchTouchEvent返回false,true则是true。假如是设置了onTouchListenr,在onTouch()里面返回了true的话,那么dispatchTouchEvent也会返回true。
View的onTouchEvent事件
1 | public boolean onTouchEvent(MotionEvent event) { |
可以看到,当View是不可用的时候,那么就交由他的一些状态的与操作决定返回值,当他设置了mTouchDelegate的时候就交由mTouchDelegate对象去处理,否则,只要是View为CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE,无论是如何处理action ,都将会返回true。
View的viewFlags默认值是0x18000000,条件2默认是为false的,除非我们设置一些操作,比如setClickListener之类的才会变成true。
ViewGroup的onTouchEvent(event)
在Viewgroup中,我们发现他并没有定义或者是override onTouchEvent()方法,只是通过用父类的super.dispatchTouchEvent去实现的。而这个调用时机就是上面分析ViewGroup的本身的dispatchTouchEvent讲到的,只要mFirstTouchTarge为空,或者是子View的dispatchTouchEvent()返回了false的时候去调用。
Activity的onTouchEvent(event)
源码是:
1 | public boolean onTouchEvent(MotionEvent event) { |
Window的shouldCloseOnTouch源码:
1 | public boolean shouldCloseOnTouch(Context context, MotionEvent event) { |
mCloseOnTouchOutside是一个boolean值,由android:windowCloseOnTouchOutside决定,isOutOfBounds是是否在UI内,peekDecorView()是返回当前根View。我们有时候需要popupWindow点击外部消失就可以使用上面的那个属性了。
一些疑问
上面Log的打印过程已经从源码角度走了一遍了,我们总结一下关于整一个View的事件传递过程,以及解析一些问题?比如我们应该怎么使得我么自己定义的ViewGroup收到除了ACTION_DOWN事件。继承APi23的Activity,我们通过View的分析看到他的View排布是:
1 | com.android.internal.policy.PhoneWindow$DecorView@ee2b93c |
针对内容区域,FrameLayout中,事件分发从Activity开始,然后到PhoneWindow,然后回到ActionBarOverlayLayout,然后是到FrameLayout,这个过程我们假如没有重写Activity中的方法是无法干预的。从FrameLayout到CustomViewGroup的事件分发过程,由于CustomViewGroup是可接受焦点的,可以被FrameLayout分发到事件。所以,假如我们需要ViewGroup中接受到除了ACTION_DOWN之外的action,按照之前的分析,在FrameLayout指向dispatchTouchEvent()的时候,DOWN的时候回清空列表,当子View(CustomViewGroup)的dispatchTouchEvent()返回了true,则会把当前的子View添加到mFirstTouchTarget链表中去。所以当CustomViewGroup的dispatchTouchEvent返回了true,FrameLayout在执行UP的时候,mFirstTouchTarget就会变成了CustomViewGroup了,默认是不会拦截了,他将会再次去执行CustomViewGroup的dispatchTouchEvent(),传递到哪里的就是UP事件了或者是MOVE事件了。但是,在CustomViewGroup的dispatchTouchEvent返回了false的时候,他是不会被添加到FrameLayout的mFirstTouchTarget中的,所以他就不会响应到UP事件。
总结
我们使用一张图来总结View的事件出传递过程:
有几点结论:
- 只有有View对ACTION_DOWN事件感兴趣,即使返回了true,其他的ACTION事件才会传递下去给他,比如我们一开始的例子中,自定义的ViewGroup就是没有收到UP事件,因为他以及他的子View都是对他不感兴趣,没有返回true,或者说是mFirstTouchTarget一直是null的。导致事件自从DOWN之后是一直被拦截的,可以看上面的ViewGroup的dispatchTouchEvent()方法说明。
- 只要在dispatchTouchEvent的过程中对于各个的View或者是ViewGroup,没有拦截掉事件(onInterceptTouceEvent返回true)或者是对事件感兴趣(OnTouchEvent返回true),那么事件就会一直被传递下去。直到View Tree最底层的View中。
- 只要是最底层的View Tree也是一直不处理,不感兴趣(不考虑激活),对于事件,那么这个事件就会一直往回传,传递给父ViewGroup的onTouchEvent,直到最顶级的Activity的onTouchEvent事件中。
4,当View对事件不感兴趣,只是单纯的super的时候,假如改View是激活的,是的在View的onTouchEvent()能够走进switch语句中的条件为true,比如设置了OnClickListener,那么就是表明他是对事件感兴趣的,只是隐式的而已,那么就可以使得ACTION_UP等事件可以向下传递。 - 只要是dispatchTouchEvent返回了true,就表明改方法自身销毁了该事件
- 假如是拦截了事件,那么消费事件将会在改View/ViewGroup的onTouchEvent中进行