全面总结之 事件传递 篇

在讲解事件传播之前,我们需要先了解一下我们的布局在屏幕中究竟是怎么显示的:

我们在写好布局文件之后,需要在 Activity 当中调用 setContentView 方法来将我们写好的布局显示,关于 setContentView 方法是如何执行的,这里就不赘述了,可以看这里:Android应用setContentView与LayoutInflater加载解析机制源码分析 - 工匠若水

既然知道了是通过 setContentView 来设置布局的,那我们来做个实验,将 setContentView 方法注释掉,看结果:

public class MainActivity extends Activity {

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

运行之后发现并不报错,依旧可以运行,只不过界面一片空白,使用 Android 提供的布局查看工具 hierarchyviewer 来看一下:

把 setContentView 的注释取消之后,如图:

套用工匠若水的一张图来展示 Activity 的结构:

解释一下这几个类:

  • PhoneWindow:Window 的子类,而 Window 是所有视图的容器,它(的实例)管理着窗口的外观、操作等。

  • DecorView:在源码中的注释:窗口的最顶层视图,包含着窗口装饰。有的文章说 DecorView 是 PhoneWindow 的内部类,但是我下载的源码该类是一个独立的类,可能是谷歌后面修改过,这个不重要,重点是该类是 FrameLayout 的子类,我们自己创建的所有布局内容,都是包含在这个容器中。

梳理一下,Activity 用来展示界面,其界面最底层是一个 PhoneWindow,而真正用来“盛放”控件的,是一个 DecorView 对象,在 DecorView 中有一个 LinearLayout 布局,其上半部分是 ActionBar,下半部分是一个 FrameLayout,这个 FrameLayout 用来“盛放”我们自己定义的布局。

事件分发

我们的布局在屏幕中是一层一层的,那么当我们的手指在屏幕上划过的时候,究竟该哪个组件对我们的操作做出响应呢?同样是滑动手指,两者嵌套的 ScrollView 该哪个做出响应呢?这里就需要了解事件的分发了,只有了解了事件分发的规则,才可以正确的处理各种操作冲突,才会在自定义组件时为其设置恰当的操作方式。

Android 将我们的操作都封装在一个 MotionEvent 对象当中,该对象包含了我们手指在屏幕上划过的所有数据,包括操作的事件、坐标、滑动的手指个数等等。

Android 又将我们的操作分解为以下几个事件:

  • ACTION_DOWN:手指在屏幕按下事件
  • ACTION_UP:手指在屏幕抬起事件
  • ACTION_MOVE:手指在屏幕移动事件
  • ACTION_POINTER_DOWN:第二根手指按下屏幕事件
  • ACTION_POINTER_UP:第二根手指从屏幕抬起事件
  • ACTION_CANCEL:特殊的事件,发生在当一个动作被一个视图处理,然后又交给其他视图处理时候会用到

所以说,我们在屏幕上的操作,都是从 ACTION_DOWN 开始,中间夹杂 N(N >= 0) 个 ACTION_MOVE,最后以 ACTION_UP 结束。从 DOWN 开始到 UP 结束,整个过程,我们称之为一个“事件序列”。

可以通过调用 MotionEvent 的 getAction 方法,来获取到当前事件。

先说事件的传递规则,事件的传递顺序是由最“顶”层的 Activity 开始的,然后依次向下传递给子元素,直到中间有控件处理了该事件,则不再往下传递,否则会一直向下,最终没有控件处理的事件,会原路返回,又回到最上层,由 Activity 处理。

有三个方法贯穿整个分发过程,他们的返回值都是 boolean 类型的,他们分别是:

  • dispatchTouchEvent(MotionEvent ev):用于事件分发。

  • onTouchEvent(MotionEvent event):用于消耗事件。

  • onInterceptTouchEvent(MotionEvent ev):用于拦截事件(此方法为 ViewGroup 独有)。

下面我们开始通过代码来看一下事件传递的规则:

先给出我们的示例代码:

MainActivity

public class MainActivity extends Activity {

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

    /**
     * Activity 消费事件方法
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("TTT", "Activity -- onTouchEvent -- ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d("TTT", "Activity -- onTouchEvent -- ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * Activity 分发事件方法
     *
     * @param ev
     * @return
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("TTT", "Activity -- dispatchTouchEvent -- ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d("TTT", "Activity -- dispatchTouchEvent -- ACTION_UP");
                break;

        }
        return super.dispatchTouchEvent(ev);
    }
}

MyLayout

public class MyLayout extends RelativeLayout {


    public MyLayout(Context context) {
        super(context);
    }

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * View 消费事件方法
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("TTT", "ViewGroup -- onTouchEvent -- ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d("TTT", "ViewGroup -- onTouchEvent -- ACTION_UP");
                break;

        }
        return super.onTouchEvent(event);
    }

    /**
     * View 分发事件方法
     *
     * @param ev
     * @return
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("TTT", "ViewGroup -- dispatchTouchEvent -- ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d("TTT", "ViewGroup -- dispatchTouchEvent -- ACTION_UP");
                break;

        }
        return super.dispatchTouchEvent(ev);
    }
}

MyView:

public class MyView extends View {

    /**
     * View 消费事件方法
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("TTT", "View -- onTouchEvent -- ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d("TTT", "View -- onTouchEvent -- ACTION_UP");
                break;

        }
        return super.onTouchEvent(event);
    }

    /**
     * View 分发事件方法
     *
     * @param ev
     * @return
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("TTT", "View -- dispatchTouchEvent -- ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d("TTT", "View -- dispatchTouchEvent -- ACTION_UP");
                break;

        }
        return super.dispatchTouchEvent(ev);
    }

    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

activity_main


<com.li_xyz.touchtest.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="300dp"
    android:background="#e77070">

    <com.li_xyz.touchtest.MyView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#09efcd" />

</com.li_xyz.touchtest.MyLayout>

布局显示如下:

下面我们来通过 Log 看一下事件究竟是如何传递、消费以及拦截的。

示例代码:
MainActivity

public class MainActivity extends Activity {

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        new EventUtil("Activity", "onTouchEvent", event);
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        new EventUtil("Activity", "dispatchTouchEvent", ev);
        return super.dispatchTouchEvent(ev);
    }
}

FatherLayout

public class FatherLayout extends RelativeLayout {

    public FatherLayout(Context context) {
        super(context);
    }

    public FatherLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FatherLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        new EventUtil("FatherLayout", "onTouchEvent", event);
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        new EventUtil("FatherLayout", "dispatchTouchEvent", ev);
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        new EventUtil("FatherLayout", "onInterceptTouchEvent", ev);
        return super.onInterceptTouchEvent(ev);
    }
}

ChildView

public class ChildView extends View {

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        new EventUtil("ChildView", "onTouchEvent", event);
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        new EventUtil("ChildView", "dispatchTouchEvent", ev);
        return super.dispatchTouchEvent(ev);
    }

    public ChildView(Context context) {
        super(context);
    }

    public ChildView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public ChildView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

EventUtil

public class EventUtil {
    public EventUtil(String viewName, String methodName, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("TTT", viewName + " --- " + methodName + " --- DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d("TTT", viewName + " --- " + methodName + " --- UP");
                break;
        }
    }
}

activity_main

<com.li_xyz.touchtest.FatherLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="300dp"
    android:background="#e77070">
    <com.li_xyz.touchtest.ChildView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="#09efcd" />

</com.li_xyz.touchtest.FatherLayout>

我们给每个方法打上 Log,查看一下事件在组件之间到底是如何传递、分发、拦截和消费的。

情形 1,返回值全部为默认值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
FatherLayout --- onInterceptTouchEvent --- DOWN
ChildView --- dispatchTouchEvent --- DOWN
ChildView --- onTouchEvent --- DOWN
FatherLayout --- onTouchEvent --- DOWN
Activity --- onTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
Activity --- onTouchEvent --- UP

可以看到,如果我们不对事件进行干涉的的话,ACTION_DOWN 会从控件树的最顶端开始,依次向下传递,经由到 ViewGroup 的时候,在分发之后,还会触发拦截方法(onInterceptTouchEvent),一直传递控件树的最底层,会从下向上依次查看该事件单元是否有控件可以消费(onTouchEvent),如果一直到最顶端,事件依旧没有被消费的话,就交由最顶层(也就是 Activity)的 onTouchEvent 方法消费。因为所有组件都没有消费 ACTION_DOWN 事件,所以事件序列的后续事件也不再往下分发,直接交由 Activity 的 onTouchEvent。


情形 2,FatherLayout 的 dispatchTouchEvent 返回值为 true,其余方法默认返回值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
FatherLayout --- dispatchTouchEvent --- UP

情形 3,FatherLayout 的 dispatchTouchEvent 返回值为 false,其余方法默认返回值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
Activity --- onTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
Activity --- onTouchEvent --- UP

情形 4,ChildView 的 dispatchTouchEvent 返回值为 true,其余方法默认返回值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
FatherLayout --- onInterceptTouchEvent --- DOWN
ChildView --- dispatchTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
FatherLayout --- dispatchTouchEvent --- UP
FatherLayout --- onInterceptTouchEvent --- UP
ChildView --- dispatchTouchEvent --- UP

情形 5,ChildView 的 dispatchTouchEvent 返回值为 false,其余方法默认返回值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
FatherLayout --- onInterceptTouchEvent --- DOWN
ChildView --- dispatchTouchEvent --- DOWN
FatherLayout --- onTouchEvent --- DOWN
Activity --- onTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
Activity --- onTouchEvent --- UP

通过上面五个案例,可以看出事件的分发规则:
● 当控件的 dispatchTouchEvent 返回值为 true 时候,该事件单元由当前控件的 dispatchTouchEvent 消费。
● 当控件的 dispatchTouchEvent 返回值为 false 时候,则事件不再向下传递,则该事件交由上层控件的 onTouchEvent 方法进行消费,如果上层控件的 onTouchEvent 不能处理,则一直上传,知道最顶层。


情形 6,FatherLayout 的 onInterceptTouchEvent 返回值为 true,其余方法默认值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
FatherLayout --- onInterceptTouchEvent --- DOWN
FatherLayout --- onTouchEvent --- DOWN
Activity --- onTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
Activity --- onTouchEvent --- UP

情形 6,FatherLayout 的 onInterceptTouchEvent 返回值为 false,其余方法默认值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
FatherLayout --- onInterceptTouchEvent --- DOWN
ChildView --- dispatchTouchEvent --- DOWN
ChildView --- onTouchEvent --- DOWN
FatherLayout --- onTouchEvent --- DOWN
Activity --- onTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
Activity --- onTouchEvent --- UP

通过上面的例子可以看出事件拦截的过程,如果事件拦截(返回值为 true),则该事件单元交由该组件的 onTouchEvent 方法消费,并且不再传递,如果该组件的 onTouchEvent 消费了该组件,则之后的剩余事件但愿也交由其,如果 onTouchEvent 没有消费,则继续向上,查看父组件的 onTouchEvent 是否可以消费。如果返回 false,则和默认一样,依旧需要传递到最底层,再向上查看 onTouchEvent 方法能否消费。


情形 7,FatherLayout 的 onTouchEvent 返回值为 true,其余方法默认值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
FatherLayout --- onInterceptTouchEvent --- DOWN
ChildView --- dispatchTouchEvent --- DOWN
ChildView --- onTouchEvent --- DOWN
FatherLayout --- onTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
FatherLayout --- dispatchTouchEvent --- UP
FatherLayout --- onTouchEvent --- UP

情形 8,FatherLayout 的 onTouchEvent 返回值为 false,其余方法默认值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
FatherLayout --- onInterceptTouchEvent --- DOWN
ChildView --- dispatchTouchEvent --- DOWN
ChildView --- onTouchEvent --- DOWN
FatherLayout --- onTouchEvent --- DOWN
Activity --- onTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
Activity --- onTouchEvent --- UP

情形 9,ChildView 的 onTouchEvent 返回值为 true,其余方法默认值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
FatherLayout --- onInterceptTouchEvent --- DOWN
ChildView --- dispatchTouchEvent --- DOWN
ChildView --- onTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
FatherLayout --- dispatchTouchEvent --- UP
FatherLayout --- onInterceptTouchEvent --- UP
ChildView --- dispatchTouchEvent --- UP
ChildView --- onTouchEvent --- UP

情形 10,ChildView 的 onTouchEvent 返回值为 false,其余方法默认值,Log 如下:

Activity --- dispatchTouchEvent --- DOWN
FatherLayout --- dispatchTouchEvent --- DOWN
FatherLayout --- onInterceptTouchEvent --- DOWN
ChildView --- dispatchTouchEvent --- DOWN
ChildView --- onTouchEvent --- DOWN
FatherLayout --- onTouchEvent --- DOWN
Activity --- onTouchEvent --- DOWN
Activity --- dispatchTouchEvent --- UP
Activity --- onTouchEvent --- UP

通过上面的例子,总结一下事件的消费过程,如果该组件的 onTouchEvent 返回值为 true,则该事件单元交由该组件的 onTouchEvent 消费,并且不在继续往下传递,并且事件序列的后续事件单元也交由其处理
如果返回 false,则和默认一样,向上传递,并且寻找可以消费(onTouchEvent)的组件


事件传递总结

  1. 事件是由 ACTION_DOWN 开始,中间夹杂若干 ACTION_MOVE,以 ACTION_UP 结束。
  2. 如果该组件没有处理 ACTION_DOWN,则该事件序列的后续其他事件单元也不会再传递给该组件。
  3. 事件默认是从控件树的最顶端依次向下分发(dispatchTouchEvent),一直传递到最底层。
    3.1. 如果该组件 dispatchTouchEvent 返回 true,则该事件单元交由该控件的 dispatchTouchEvent 消费,并且后续事件单元也会分发至该组件的 dispatchTouchEvent 进行消费。
    3.2. 如果该组件的 dispatchTouchEvent 返回 false,则交由上层控件的 onTouchEvent 进行处理,如果上层可以处理则罢,如果不能消费,则继续上传。
  4. 如果事件被 ViewGroup 拦截(onInterceptTouchEvent 返回 true),则该事件不再向下传递,直接交由该组件的 onTouchEvent 方法进行消费,如果可以消费则罢,如果不能消费,则原路向上寻求消费。
  5. 如果事件再某个节点被消费(onTouchEvent 返回 true),则事件序列的其他事件单元会依次分发至该组件,交由其 onTouchEvent 进行消费,不再向下传递。
  6. View 的 android:clickableandroid:longClickable 属性也影响着事件的消费,二者有一个为 true,该 View 则可以消费事件,android:longClickable 默认 false,而 android:clickable 分情况,例如 Button 默认为 true,TextView 默认为 false。
Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1