Android事件处理机制轻量级源码分析


正确理解Android中的事件分发和拦截机制,对于在多个ViewGroup和View嵌套以及自定义控件的时候能够正常处理用户行为至关重要。

如果要把源码一行一行看,研究事件处理机制就要到天荒地老了。这是为什么这篇博客叫轻量级源码分析,简单易懂。屏蔽掉了冗长的无需太过关心的源码,把握一下几行重点,其实事件处理就可以有一定的了解了。这篇博文就是根据源码一步一步走了一下事件处理的流程,分模块来看应该思路应该会比较清晰一点~

事件分发和拦截101

开始之前首先明确一下,在源码当中,理解事件分发,关注一下dispatchTouchEvent()方法;理解事件拦截,再多关注一个onInterceptTouchEvent()方法即可~

还有噢,dispatchTouchEvent()方法返回true表示响应全部事件,而返回false则表示只响应第一个事件,忽略后续的事件~onInterceptTouchEvent()方法返回true表示拦截事件,子View不会接收到该事件;返回false表示不拦截事件,向子View传递该事件,让子View尝试处理~

哟西~

Let`s hacking…

一、View的事件处理机制

View中的事件分发对于本身可以被点击和不可被点击的View有些许不同~

1. View的事件分发之不可被点击的View

先看一个小示例哟~这一小段代码中有两个控件,一个ImageView,一个Button,这两个控件最终都是继承View的,就用这两个控件来阐述一下View的事件处理机制

这一小段代码,还是粘上来吧~

imageView.setOnTouchListener(new OnTouchListener() {
    @SuppressLint("ClickableViewAccessibility")
    @Override 
    public boolean onTouch(View v, MotionEvent event) {
    System.out.println("imageView eventAction = "+event.getAction());
    return false;
}

先说一说本身不可被点击的ImageView~这里给imageView添加一个onTouchListener,我们看看在return false的情况下,单击一次,打印了几次输出信息~

运行之后发现输出了imageView eventAction = 0。由于一次单击包括按下和抬起两个操作(0表示按下的操作,1表示抬起的操作),因此从这个现象可以看出,抬起的操作在这个情况下没有被监测到。想知道为什么,就来看看源码咯~

ImageView是继承View的(继承了dispatchTouchEvent()方法),所以应该到View的源码中查看一下dispatchTouchEvent()(我们要关注的就是这个方法嘛)方法,看看View是怎么进行事件分发的(就知道ImageView是怎么处理事件的了)。过滤了不必要的代码之后,应该关注的代码如下:

dispatchTouchEvent()方法

public boolean dispatchTouchEvent(MotionEvent event) {
    ....
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

View的dispatchTouchEvent()方法中,这个if语句的意思是说,如果mOnTouchListener 引用不为空,并且当前要接收事件的控件(此处就是这个IamgeView)处于可用状态(只要不设置setEnable = false,这个条件是始终成立的,所以后面就不说这个条件了),并且mOnTouchListener这个引用的onTouch方法返回了true,那么整个dispatchTouchEvent()就返回true,否则返回onTouchEvent(event)方法执行的结果。

那么ImageView被点击之后,是否执行了if语句呢?下面来判断一下if中的条件该ImageView是否满足~

1. `mOnTouchListener `引用不为空
View中有一个

public void setOnTouchListener(OnTouchListener l) {
    mOnTouchListener = l;
}

的方法。在示例代码中,ImageView是调用了这个方法的。因此,匿名内部类OnTouchListener()被作为参数l赋值给了mOnTouchListener 。那么mOnTouchListener 引用不为空条件成立;

2. 当前控件可用
此条件始终成立;

3.mOnTouchListener 这个引用的onTouch方法返回了true
mOnTouchListener指向的是当前的imageView的OnTouchListener()监听器。看ImageView的监听器,我们在监听器的onTouch方法中,返回了false,因此该条件不成立;

所以呢,此时的ImageView是不会执行dispatchTouchEvent()方法中的if语句块的,而会执行其所继承的onTouchEvent(event)方法,并返回相应的布尔结果。

到这里,还无法得知为什么ImageView上的抬起事件没有被监测到。那就来看看这个onTouchEvent(event)方法到底干了什么。这个方法代码超长的,哈哈,轻量级源码如下:

onTouchEvent()方法

public boolean onTouchEvent(MotionEvent event) {
       ....
    if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
               ...
        }
        return true;
    }
    return false;
}

这里又有一个if语句块,这个if说,如果你这个接收事件的控件可以被点击,或者可以被长点击,那么就执行if中的逻辑,然后返回true,否则整个方法返回false。那就来看看我们的ImageView是否满足条件咯~

挂了,ImageView本身是没有被点击的能力的,所以这个if语句不会被执行;也就是说,此时的onTouchEvent()方法返回了false

然后再回到dispatchTouchEvent()方法,既然此时的ImageView不能执行if中的代码,而onTouchEvent()方法又返回了false,所以整个dispatchTouchEvent()就返回了false。根据文章开头所说dispatchTouchEvent()返回true和false的意义得知,这个单击事件整体只有按下会被处理,所以就只会打印一次输出信息咯~

如果想让ImageView同时响应按下和抬起的事件,该怎么办呢?有两个方式:

1.在OnTouchListener()的onTouch()方法中返回true;那么ImageView将满足dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent event) {
    ....
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

方法的if语句中的所有条件;执行if语句,最终dispatchTouchEvent()返回true,单击事件整体被响应,打印两次输出信息;

2.ImageView的onTouch()方法仍然返回false,同时为ImageView再添加一个setOnClickListener();View中的setOnClickListener()方法的源码做了这么一件事情

public void setOnClickListener(OnClickListener l) {
    //如果当前控件没有点击事件,设置一个点击事件
        if (!isClickable()) {
            setClickable(true);
        }
        ...
}

如果当前控件不可被点击,则设置其为可点击;这样一来,ImageView就满足了onTouchEvent()方法

public boolean onTouchEvent(MotionEvent event) {
   ....
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
            ...
        }
        return true;
    }
    return false;
}


中if语句可点击的条件;进入了if语句之后,不管switch里面执行了多少代码,在switch结束之后,使用返回了true;所以,单击事件被整体响应,打印两次输出信息~

2. View的事件分发之可以被点击的View

接下来就说说Button这个东西~

button.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        System.out.println("Button eventAction = "+event.getAction());
        return false;
    }
});

在同样的环境下,同样地为Button添加了一个触摸事件监听器,在onTouch()方法中返回了false;运行之后,输出了Button eventAction = 0和Button eventAction = 1。这里同样返回false,为什么就能打印两次信息,为什么Button的整个单击事件可以被完全监测呢?

原因就在于Button本身是可以被点击的;在其他条件都相同的情况下,Button本身可以被点击的属性,可以让其执行onTouchEvent()方法中的if语句

onTouchEvent()方法

public boolean onTouchEvent(MotionEvent event) {
       ....
    if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
               ...
        }
        return true;
    }
    return false;
}

从而返回true;进而dispatchTouchEvent()方法返回true,单击事件的按下和抬起都会被监测~

3. onClick()、onTouch()和onTouchEvent的调用顺序

上面一点很好理解。那进一步,如果也给Button再来一个setOnClickListener()呢?就像这样

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        System.out.println("button onclick");
    }
});

在onTouch()方法返回false的情况下,还会打印"button onclick"的信息吗?

运行之后发现,"button onclick"的信息在Button eventAction = 0和Button eventAction = 1之后输出了。

但是为什么是在onTouch()方法之后呢?

那就继续看源码咯~

这里直接看onTouchEvent()方法

onTouchEvent()方法

public boolean onTouchEvent(MotionEvent event) {
       ....
    if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
               ...
               performClick();
               ...
        }
        return true;
    }
    return false;
}

由于Button的onTouch()方法返回了false,导致dispatchTouchEvent()方法的if语句不会执行;而会执行if下面的onTouchEvent()方法(对于这个逻辑请参看ImageView的讲解)。到了onTouchEvent()方法中,又由于Button本身可以被点击,显然会进入if语句执行;注意到在这个onTouchEvent()的switch语句处理抬起操作的代码中,有一个performClick()方法;顺势找到performClick()方法的源码~

performClick()

public boolean performClick() {
    ... 
    if (mOnClickListener != null) {
        ....
        mOnClickListener.onClick(this);
        ...
    }
    ...
}

performClick()的源码主要部分,在mOnClickListener不为空的情况下,调用了mOnClickListener身上的onClick()方法;来看看这个mOnClickListener在什么地方赋值,指的是什么~还记得View中的setOnClickListener()方法吗~

setOnClickListener()

public void setOnClickListener(OnClickListener l) {
    ...
    mOnClickListener = l;
}

原来在为Button添加click监听事件的时候,new出来的匿名内部类OnClickListener()传递给了setOnClickListener(),并赋值给了mOnClickListener;因此,mOnClickListener指的就是OnClickListener(),其在performClick()方法中调用的onClick()方法,就是Button处理点击事件所执行的onClick()方法。

这么一来,输出顺序之谜就明了了。

首先,在Button被点击的时候,执行了其监听器中的onTouch()方法;接着,onTouch()方法返回了false,就执行了onTouchEvent()方法;再接着,由于Button满足了if条件,进入switch语句,调用了performClick()方法,最终执行了onClick()方法;调用顺序即:

onTouch() --> onTouchEvent() --> onClick()

至此,View的事件分发就很清楚了~以后触摸事件和点击事件,在处理的时候,就不会蒙圈咯~

在了解了View的事件分发机制之后,再来看看ViewGroup对于事件是怎么进行处理的。

二、ViewGroup的事件处理机制

大家都知道,说到ViewGroup的事件处理机制,就有那么一张图说ViewGroup的事件是一级一级往下传的,传到最后消费了就消费了,不消费再传回来云云~但是底层的实现不得而知的~真相弄明白,还是扎到源码看看~

同样以一个小示例来阐释一下ViewGroup的事件处理机制~

先来一个自定义Layout继承LinearLayout~

MyLayout

public class MyLayout extends LinearLayout {
    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //此时该方法返回false
        return false;
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }
}

在MyLayout中放置一个按钮

activity_main.xml

<com.example.viewgrouponclickdmeo.MyLayout
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_world" />
 </com.example.viewgrouponclickdmeo.MyLayout>

在MainActivity中进行测试

MainActivity

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    layout = (MyLayout) findViewById(R.id.layout);
    button = (Button) findViewById(R.id.button);


    layout.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
        Log.i(tag, "click   layout ...........................");
        }
    });

    button.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
        Log.i(tag, "click   button ...........................");
        }
    });
}

为MyLayout和Button添加了点击事件,输出相应内容~

来小小分析一下~

首先,这个小示例自定义了一个MyLayout,继承了LinearLayout;而LinearLayout继承自ViewGroup,因此这个MyLayout最终是继承ViewGroup的;

其次,MyLayout重写了ViewGroup的onInterceptTouchEvent(MotionEvent ev)方法和dispatchTouchEvent(MotionEvent ev)方法,重新定义了事件的处理规则;这里在onInterceptTouchEvent(MotionEvent ev)中返回了false;

最后,在MainActivity运行之后,发现当MyLayout的onInterceptTouchEvent(MotionEvent ev)方法返回false的时候,打印了"click button ………………………";而当返回true的时候,打印了"click layout ………………………";

为什么onInterceptTouchEvent(MotionEvent ev)的返回值决定了输出内容呢?来看看onInterceptTouchEvent(MotionEvent ev)方法在源码中的作用~

onInterceptTouchEvent(MotionEvent ev)在dispatchTouchEvent(MotionEvent ev)方法中被调用~

上源码~

ViewGroup中的dispatchTouchEvent(MotionEvent ev)方法

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (action == MotionEvent.ACTION_DOWN) {
        //让mMotionTarget始终为null
        if (mMotionTarget != null) {
            mMotionTarget = null;
        }
        //onInterceptTouchEvent返回false,说明向下传递
        //onInterceptTouchEvent返回true,说明拦截
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {

            //1. 找到当前控件的子控件
            //2. 判断当前事件所触发的位置(手指的位置)所在的坐标值(x,y)在哪个子控件的内部
            //3. 判断当前子控件是ViewGroup的子类对象,还是View的子类对象
                //3.1 如果是ViewGroup的子类,那么再重复以上逻辑,继续寻找子控件
                //3.2 如果是View的子类,就尝试去处理该事件
                    //3.2.1 如果View的子类的dispatchTouchEvent()方法返回true,事件被响应,方法完结
                    //3.2.2 如果View的子类的dispatchTouchEvent()方法返回false,继续走下面的逻辑

        }
    }
    ...
     target = mMotionTarget
    //target一定是null
    if (target == null) {
        ...
        //调用当前viewgroup的view处理事件的方法
        return super.dispatchTouchEvent(ev);
    }
   ...
}

1. 解决disallowIntercept~

先找到if (disallowIntercept || !onInterceptTouchEvent(ev)) 这一行~通过观察这一行代码可以发现,onInterceptTouchEvent(ev)方法是否起作用,取决于disallowIntercept的值;只有当disallowIntercept的值为false的时候,onInterceptTouchEvent(ev)方法的返回值才能生效,否则无需判断onInterceptTouchEvent(ev)方法,if语句始终会执行。那我们先解决掉这个disallowIntercept~

disallowIntercept的值代表着是否允许拦截触摸事件~false表示拦截,true表示不拦截~

dispatchTouchEvent(MotionEvent ev)方法的源码中有这么一句话

boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

这句话为disallowIntercept变量赋了初始值;这个初始值是多少呢?

先来看看这个mGroup的值是多少~

protected int mGroupFlags;

源码中只声明了mGroupFlags,因此mGroupFlags的默认值为0;

再来看看FLAG_DISALLOW_INTERCEPT常量是多少~

protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000;

原来FLAG_DISALLOW_INTERCEPT被声明为0x80000。

所以就可以得到

boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

中mGroupFlags & FLAG_DISALLOW_INTERCEPT的值是0(0 & 0x80000 = 0,0 & 上任何数都是0);意味着disallowIntercept = false(0 != 0),也就意味着onInterceptTouchEvent(ev)方法在disallowIntercept是默认值的情况下是起作用的~

2. 关于disallowIntercept

ViewGroup的源码中有这么一个方法

requestDisallowInterceptTouchEvent()方法

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if(disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0) {
        //we're already in this state, assume our ancesters are too
        return;
    }
    if(disallowIntercept) {
        //相当于mGroupFlags = FLAG_DISALLOW_INTERCEPT
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        //相当于mGroupFlags = 0
        mGroupFlagfs &= ~FLAG_DISALLOW_INTERCEPT;
    }
    ...
}

我们可以通过这个方法来设置disallowIntercept的默认值;如果大家想永远不拦截触摸事件,让事件往子View传递的话,直接复写这个方法,传入一个true即可~

3. 判断onInterceptTouchEvent(MotionEvent ev)方法返回值

3.1 onInterceptTouchEvent(ev)方法返回false

继续看源码,可以看到,if (disallowIntercept || !onInterceptTouchEvent(ev)) 这一行对onInterceptTouchEvent(ev)做了取非的处理;示例代码中MyLayout复写的onInterceptTouchEvent(ev)方法,返回了false;那么这里取非,也就是!onInterceptTouchEvent(ev) = true;所以会执行if语句中的代码;将if语句中的代码逻辑归纳为以下几点(如注释):

//1. 找到当前控件的子控件
//2. 判断当前事件所触发的位置(手指的位置)所在的坐标值(x,y)在哪个子控件的内部
//3. 判断当前子控件是ViewGroup的子类对象,还是View的子类对象
    //3.1 如果是ViewGroup的子类,那么再重复以上逻辑,继续寻找子控件
    //3.2 如果是View的子类,就尝试去处理该事件
        //3.2.1 如果View的子类的dispatchTouchEvent()方法返回true,事件被响应,方法完结
        //3.2.2 如果View的子类的dispatchTouchEvent()方法返回false,继续走下面的逻辑

看到这里,相信大家一定明白了为什么说ViewGroup处理事件会一级一级往下分发,就是因为上述的第一步;

而为什么有第二步,我们用一个图来解释~

点击事件

如图,当点击事件发生的时候,系统会记录点击发生地点的x,y坐标值;例如,如果是点击事件1,那么最外层的ViewGroup往下分发事件的时候,找到了两个子View,到底哪个子View该处理这个事件呢?就根据点击事件1的坐标来判断。当前坐标在上面这个子View中,所以应该有它来负责处理这个事件;如果是点击事件2,ViewGroup分发事件后找不到对应的子View可以处理这个事件,就会返回ViewGroup尝试自己处理。

第二步完结,就会判断该子控件是ViewGroup的子类还是View的子类,来决定是否继续分发事件还是就地处理~

如果继续分发,就再从第一步逻辑开始执行;如果就地处理,返回true,代表该事件被该子View处理完毕;返回false,表示该子View没有处理该事件;此时该事件会沿着原路向其父控件传递,直至有控件处理;如果到顶部的父控件都不处理该事件,则该事件交由View的dispatchTouchEvent()方法做默认处理~

回到我们的示例~根据上面的分析,当我们点击Button按钮的时候,事件会从MyLayout向下分发(由于onInterceptTouchEvent(ev)方法返回的是false),分发到Button控件的时候,由于Button已经是最后一级子View,系统发现点击的地方的x,y坐标在Button按钮之内,又因为Button是View的子类,所以判定应该由Button来处理该事件;此时,根据上面讲述的View的事件处理机制,在Button没有复写dispatchTouchEvent()方法的情况下,其dispatchTouchEvent()始终返回true;因此,根据3.2.1步所述, 如果View的子类的dispatchTouchEvent()方法返回true,事件被响应,方法完结,打印出"click button ………………………";

3.2 onInterceptTouchEvent(ev)方法返回true

反之,如果MyLayout复写的onInterceptTouchEvent(ev)方法返回了true,将会是什么情况呢?

再找到if (disallowIntercept || !onInterceptTouchEvent(ev)) 这一行~如果onInterceptTouchEvent(ev)方法返回了true,那么if语句将不会执行~这时就要跳出if语句,继续看下面的代码~

关键代码

...
target = mMotionTarget
    //target一定是null
    if (target == null) {
    ...
    //调用当前viewgroup的view处理事件的方法
    return super.dispatchTouchEvent(ev);
}
...

这段代码的注释中写到,//target一定是null,原因在这~

在ViewGroup的dispatchTouchEvent(ev)方法中有这么一段逻辑(见上述ViewGroup源码)

//让mMotionTarget始终为null
if (mMotionTarget != null) {
    mMotionTarget = null;
}

如果mMotionTarget不等于空,就将mMotionTarget置为空。由于这个逻辑是在判断是否拦截(if (disallowIntercept || !onInterceptTouchEvent(ev)))事件的逻辑之前。因此,这个mMotionTarget是始终为null的。所以

...
target = mMotionTarget
    //target一定是null
    if (target == null) {
    ...
    return super.dispatchTouchEvent(ev);
}
...

这段代码的if语句一定会执行。if语句中,调用了当前控件的父控件的dispatchTouchEvent(ev)方法来处理该事件;当前控件的父控件,就是View,所以这里又可以参看View的事件处理机制的逻辑,来处理这次触摸或者点击事件了。要注意的是,这里所说的父控件,不是布局中的父控件,而是Java代码中所继承的父类;无论是继承ViewGroup还是View的子控件,其父控件都是View。因此,这时就相当于MyLayout拦截了该点击事件,调用了其最终父类View中的dispatchTouchEvent(ev)方法,处理了点击事件,输出了"click layout ………………………"


三、总结

从View的事件处理机制中可以得出

结论1:如果当前控件继承自View,想要事件被完全监听,必须保证其dispatchTouchEvent()方法返回true;因此,可以选择在触摸事件监听器中返回true或者使控件具备可被点击的能力即可;


从Button同时添加触摸事件和点击事件的示例可以得出

结论2:无论当前是什么控件,onClick()事件都是在onTouchEvent()方法中被响应的;响应onClick()事件时的事件处理机制方法的调用顺序为:onTouch()(设置了onTouchListener,并在onTouch()方法中返回false) --> onTouchEvent()(dispatchTouchEvent()方法不走if的逻辑时) --> onClick()(peformClick()方法);


从ViewGroup的事件处理机制中可以得出

结论3:其实onInterceptTouchEvent(MotionEvent ev)决定了ViewGroup中的事件是否往其子View中传递;在不复写dispatchTouchEvent(MotionEvent ev)方法的前提下(不改变父类处理逻辑),onInterceptTouchEvent(MotionEvent ev)返回true,事件被拦截;返回false,事件往子View传递;

结论4:没有任何控件响应的事件,会调用View的dispatchTouchEvent()方法做默认处理~

理解事件分发和拦截的逻辑比较跳跃,写这篇博文也很慎重,检查了很多次,希望自己在复习知识的同时,能简化一些流程,帮助大家一起啃一啃这块硬骨头~如果不慎还是出现错误,请大家指出,我们一起讨论,一起进步~~