有些困难无法逃避,没办法,那就只有去解决它。view事件分发对我而言是一块很难啃的骨头,看了《安卓开发艺术探索》关于这个知识点的讲解,看了好几遍,始终不懂,最终通过调试分析结果,看博客,再回过头看,总算能了解个大概。真的只能说大概,因为我在理解的过程中,还是会刻意忽略掉不少我不懂的又会诱导我深入分析的知识点,这些知识点就像歧路亡羊中的歧路,当我在不断分叉的歧路中走的越远,我离要找的羊也就越远,羊就是对整个分发体系的整体把控。就说到这了,开始探索之旅吧!
1.事件从Activity如何分发至主布局xml文件生成的view树。
//所有触摸事件先从activity中的这个方法开始。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//这是个空方法,不用管
onUserInteraction();
}
//第一步:
//所以事件的分发都先从底下的这个if判断中展开,这个方法必走,注意先会让Activity附属的window进行分发,
// 如果返回true,那么事件循环结束
//如果返回false,那么就调用底下的onTouchEvent.这里重点要看wondow是怎么把事件分发下去的。
//window作为抽象类,从它的注释介绍中得知它的唯一实现类是PhoneWindow,接着看这个类的superDispatchTouchEvent
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
以下是PhoneWindow中的superDispatchTouchEvent()方法
//第二步:
//phonewindow将事件传递给了mDecor,他是类的成员变量。位于144行,接着看DecorView
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
以下是DecorView中的superDispatchTouchEvent()方法
//第三步:DecorView继承FrameLayout,FrameLayout又是继承于ViewGroup,
// 这个FrameLayout就是我们日常写的activity的父布局,在ViewGroup中重写了
//dispatchTouchEvent,快去看看
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView是一个FrameLayout布局,它由上下两部分组成,上面是actionBar,下面是我们最最亲爱的在setContentView()方法中传进xml布局文件,生成的视图组。上面的事件已经分发至DecorView了,现在我们将样式设为为noActionBar,那么DecorView布局中就只有一个contentView布局。在上面的方法中,由于FrameLayout没有重写分发方法,所以会接着向上查找分发方法,最终找到ViewGroup中的dispatchTouchEvent方法,而这个viewGroup中的第一个子view就是contentView生成的视图组。接下来看看viewGroup中的分发方法。说实话,前面铺垫了那么多,完成可以当课外知识了解,毕竟我们经常会默认事件分发就是从contentView这个视图组进行分发的。好,来看重头戏:(为了减轻阅读压力,以下是阉割版)
TouchTarger mFirstTouchTarget = null;
//每一个MotionEvent包括一个dowm,n个move,一个up,这一系列的action,都会触发这个方法,
//所以这个方法被调用的次数就是(1+n+1)
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean intercepted = false;
//第一步:判断viewGroup自己是否要拦截,如果不拦截一般存在两种情况:
//1.用户的第一个动作是按下
//2.mFirstTouchTarget有值,从后面得知,当前的viewGroup中的某个子view处理了这个事件,mFirstTouchTarget才被附上值。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//不重写的话,onInterceptTouchEvent默认返回false
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}
//第二步:viewGroup自己不拦截,就把事件下发给它的子view
if (!intercepted){
//遍历viewGroup底下的所有子view
for (int i = childrenCount - 1; i >= 0; i--) {
//省略代码:如果子view不再当前点击区域内,continue;
//能走到这里的,表示子view都在点击区域内,所以事件能传递给它
//这里也分两种情况:
//1.child为viewGroup,就在这将整个方法重调一次
//2.child 为view,view重写了分发方法,代码在后面会贴出,先提一下它的特征:
//view没有子元素下发,所以它的dispatchTouchEvent就只是处理事件。
if (child.dispatchTouchEvent()) {
//能走到这里的,表示事件被处理。
mFirstTouchTarget = object;
break;
}
}
}
//第三步:viewGroup自己处理,分两种情况:
//1.intercepted = true;表示一开始viewGroup就决定自己处理
//2.intercepted = false,结果遍历完子元素他们都没处理,导致mFirstTouchTarget = null
if (mFirstTouchTarget == null) {
//由于viewGroup继承view,所以这里也是调用view的分发方法,来处理事件。
handled = super.dispatchTouchEvent(ev);
}
return handled;
}
上面的精简代码,多看几遍,相信你能了解viewGroup对motionEvent的分发有个清晰的了解。已经可以看到不管事件如何传递,最终都会调用到view.dispatchTouchEvent方法,我们在看看它的逻辑。先来个定心丸,最难的代码就是上面那部分,接下来的都是小菜:
//view
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false;
//当前view是否设置了OnTouchListener,以及监听事件中的onTouch()
//是否返回ture
if (li.mOnTouchListener != null
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果上面的if成立,那就没这个if什么事了。所以onTouchEvent也就不会调用。
//当然,我们也要看看result=false;进入onTouchEvent瞅一瞅:
if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
public boolean onTouchEvent(MotionEvent ev){
boolean clickable = 可点击 or 可长按;
if (clickable) {
switch (action){
//从这看出,我们最常用的clickListenr在手指抬起才会触发
case up:
if (li != null && li.mOnClickListener != null) {
li.mOnClickListener.onClick(this);
}
break;
case down:
//
break;
//其他情况
}
//这个return true很容易被人忽视,也是说只要可点击,最终onTouchEvent都会消耗这个事件
return true;
}
return false;
}
纵观view的分发方法,说白了就是处理MotionEvent,而且view也没有onInterceptTouchEvent()方法。至此,源代码解析完了,我们看用个实例来增加理解。我们自定义一个LinearLayout和Button。
public class MyLayout extends LinearLayout {
private static final String TAG = "DispatchActivity";
public MyLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG,"layout "+MotionEvent.actionToString(ev.getAction()));
//这里的return值会做修改
return true;
}
}
Button:
public class MyButton extends Button {
private static final String TAG = "DispatchActivity";
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "button: "+MotionEvent.actionToString(event.getAction()));
//这里的return值会做修改
return true;
}
}
xml布局
<com.lq.testlayoutinflate.dispatchevent.MyLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.lq.testlayoutinflate.dispatchevent.MyButton
android:id="@+id/btn_DispatchActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="click me"/>
</com.lq.testlayoutinflate.dispatchevent.MyLayout>
我们要测的分3种情况:1.MyLayout 的dispatchTouchEvent()返回ture,Button的 dispatchTouchEvent()分别返回true,false,super.dispatchTouchEvent();
前面说过,我们的activity生成的contentView是添加到FrameLayout中的:所以第开始的分发是从FrameLayout的父类viewGroup开始分发的。当我们在按钮上按钮,然后进行移动,在抬起手指,这是的log日志:这里的3种情况日志是一样的
layout ACTION_DOWN
layout ACTION_MOVE
layout ACTION_MOVE
layout ACTION_MOVE
layout ACTION_UP
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean intercepted = false;
//以1起始为第一次进入这个方法:
//1.0:第一个动作按下,这里符合,进入if
//2.0:mFirstTouchTarget有了值,再次进入if
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//onInterceptTouchEvent默认返回false。
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}
//1.1:走到这,说明FramaLayout没有拦截:
//2.1:接下来的和上一次情况类似,继续输出不同动作的log
if (!intercepted){
//1.2:FrameLayout就只有一个子节点,即contentView,也就是我们xml的根布局MyLayout
for (int i = childrenCount - 1; i >= 0; i--) {
//1.3: MyLayout输出layout ACTION_DOWN日志,然后返回true,为mFirstTouchTarget附上了值。
if (child.dispatchTouchEvent()) {
mFirstTouchTarget = object;
break;
}
}
}
if (mFirstTouchTarget == null) {
handled = super.dispatchTouchEvent(ev);
}
return handled;
}
可以看到,父布局MyLayout重写了分发方法,也就是它处理了事件,所以mFirstTouchTarget指向了它。button没有得到分发事件。
第二种情况:
1.MyLayout 的dispatchTouchEvent()返回false,Button的 dispatchTouchEvent()分别返回true,false,super.dispatchTouchEvent();3种情况下的log日志始终为:
layout ACTION_DOWN.这时的日志与上次有所不同,它的move和up事件都没有传递到MyLayout.这是怎么回事,我们再来分析下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean intercepted = false;
//1.0:第一个动作按下,这里符合,进入if
//2.0:mFirstTouchTarget==null,actionMasked ==move or up,使得进入else
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//onInterceptTouchEvent默认返回false。
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}
//1.1:走到这,说明FramaLayout没有拦截:
//2.1 intercepted = true;不进入if
if (!intercepted){
//1.2:FrameLayout就只有一个子节点,即contentView,也就是我们xml的根布局MyLayout
for (int i = childrenCount - 1; i >= 0; i--) {
//1.3: MyLayout输出layout ACTION_DOWN日志,然后返回false,所以mFirstTouchTarget依旧为null。
if (child.dispatchTouchEvent()) {
mFirstTouchTarget = object;
break;
}
}
}
if (mFirstTouchTarget == null) {
//1.4:调用:view.dispatchTouchEvent处理事件,由于这个方法我们未重写,也不会有log输出
//2.2同上,无log输出
handled = super.dispatchTouchEvent(ev);
}
return handled;
}
第三种情况:
1.MyLayout 的dispatchTouchEvent()返回super.dispatchTouchEvent(),Button的 dispatchTouchEvent()分别返回true,false,super.dispatchTouchEvent();3种情况下的log日志分别为:
layout ACTION_DOWN
button: ACTION_DOWN
layout ACTION_MOVE
button: ACTION_MOVE
layout ACTION_MOVE
button: ACTION_MOVE
layout ACTION_UP
button: ACTION_UP
---------------------------------
layout ACTION_DOWN
button: ACTION_DOWN
-------------------------------
layout ACTION_DOWN
button: ACTION_DOWN
layout ACTION_MOVE
button: ACTION_MOVE
layout ACTION_MOVE
button: ACTION_MOVE
layout ACTION_UP
button: ACTION_UP
(1)第一种情况分析:dispatchTouchEvent()返回super.dispatchTouchEvent(),Button的 dispatchTouchEvent()返回true
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean intercepted = false;
//1.0:FrameLayout第一个动作按下,这里符合,进入if
//2.0 MyLayout第一个动作按下,这里符合,进入if
//3.0 接下来的动作时move,由于mFirstTouchTarget!=null,所以进入if
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//onInterceptTouchEvent默认返回false。
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}
//1.1:走到这,说明FramaLayout没有拦截:
//2.1:走到这,说明MyLayout没有拦截:
//3.1:接下来的情况同上。
if (!intercepted){
//1.2:FrameLayout就只有一个子节点,即contentView,也就是我们xml的根布局MyLayout
//2.2:MyLayout 就只有一个子节点,即MyButton。
for (int i = childrenCount - 1; i >= 0; i--) {
//1.3: MyLayout输出layout ACTION_DOWN日志,然后返回super.dispatchTouchEvent,
//现在进入下一个方法块2.0,(执行完2.4再看这里,接受到2.4返回的false,由于只有button这一个子节点,循环结束,这次执行1.4)。
//2.3: MyButton输出button ACTION_DOWN日志,然会返回true。之后,mFirstTouchTarget被附上了值。
if (child.dispatchTouchEvent()) {
mFirstTouchTarget = object;
break;
}
}
}
//1.4:直接返回handled,无log日志输出。
//2.4: handled = false,返回至1.3处,
if (mFirstTouchTarget == null) {
handled = super.dispatchTouchEvent(ev);
}
return handled;
}
(2)第二种情况分析:dispatchTouchEvent()返回super.dispatchTouchEvent(),Button的 dispatchTouchEvent()返回false
其实这种情况与第一种情况有两处注释修改一下,读者应该就一目了然了。
//2.3: MyButton输出button ACTION_DOWN日志,然会返回false。之后,mFirstTouchTarget仍未null。
//3.0 接下来的动作时是move,并且mFirstTouchTarget==null,所以不进入if,intercepted = true。
(3)第三种情况分析:dispatchTouchEvent()返回super.dispatchTouchEvent(),Button的 dispatchTouchEvent()返回super.dispatchTouchEvent()。
从log日志种可以看到与第一种情况是一样的。也就是说button分发方法中的 return super.dispatchTouchEvent()默认就是返回ture。为啥这样,看看代码你就会明白。首先我们先回到
注释2.3处:
//MyButton输出button ACTION_DOWN日志,然会返回return super.dispatchTouchEvent()。
这时就会调用view.dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false;
//由于MyButton没有设置点击事件,所以这里的if不满足
if (li.mOnTouchListener != null
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//result=false;如果onTouchEvent也返回ture,那结果不就和return true如出一辙了。
if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
读者可以再回头看看上面的onTouchEvent()方法,在这个方法中,这要view是可以点击的,那么这个方法默认就会返回true。在该方法的最后一行注释中,我特地提到过,就是它太容易被人忽视了。
到这,例子的9种子情况都分析完了,代码不多,注释倒写了一箩筐,主要要帮助各位理解。毕竟程序员最讨厌的两件事就是写代码时加注释和看没有注释的代码。还有关于怎么处理事件分发,我没有体到,我认为只有你懂得了以上这些基本分发流程,再去看别人怎么解决滑动冲突的代码,脑子就不会那么迷茫了。