安卓事件分发

Android 的事件分发机制

2019-10-22  本文已影响0人  彭空空

导读

Android 的事件分发机制

事件分发的定义

在Android中的事件分发,就是将点击事件传递到某个具体的View的进行处理的整个过程。

举个不恰当的列子,你打开手机通讯录,找到你的女神,点击了她的电话,然后手机就会进入拨打界面的这一整个过程。你最终会通过手机接收到女神是否接了电话都有一个直观的反馈。
那么我们重点关注事件分发的顺序,和核心的方法:

事件分发的顺序

在Android中UI通常是Activity+ViewGroup+View组合而成

类型 简介 说明
Activity 控制生命周期、处理事件 统筹视图的添加、显示;与Window、View交互
View 所有UI组件的父类 Button、TextView等控件都是View的子类
ViewGroup 含多个View的容器 也是View的子类;是所有布局的父类

事件分发则先到Activity,再到容器(ViewGroup),再精准到具体View。具体的View往往是实际处理者。

事件分发的核心方法

dispatchTouchEvent() 、onTouchEvent()、onInterceptTouchEvent()。

方法 功能 说明
dispatchTouchEvent() 分发点击事件 当点击事件能够传递给当前View时,调用该方法
onTouchEvent() 处理点击事件 在dispatchTouchEvent()内部调用,false表示不处理事件
onInterceptTouchEvent() 事件拦截 在ViewGroup的dispatchTouchEvent()内部调用,ViewGroup独有的方法

结合事件分发的顺序和核心方法,这里引用一张经典的流程图:

事件分发之张经典的流程图
总结一下,整体流程就是到了每个层面,拦不拦截,处不处理,如果不拦截不处理,最终回流到Activity行成闭环, 事件分发机制就是逐级逐层,去寻找一个有消费能力的。

举个不恰当的列子,
老板Activity某天醉酒时接了一个“xxxx”相关的项目,然后交给了项目总监ViewGroup985,
项目总监ViewGroup985一听这玩意儿没经验,就问了问下面最得力的项目经理ViewGroup996会不会搞
项目经理ViewGroup996听的一脸尴尬,就叫来了主管ViewGroup035
主管ViewGroup035说我还是问问小组长吧
小组长View251一脸懵逼,就去问了程序员View404
程序员View404给小组长View251说这事搞不了
小组长View251等逐级上报最后项目总监ViewGroup985跟老板说这货公司干不了
妥妥的的一个闭环,其中任何一个环境说能干的这事儿就执行下去了

现在我们对事件分发机制有了一个清晰的认识,那么源码具体是如何实现的呢?

事件分发的源码分析

老司机都知道在Android中的顶层View其实是抽象类Window,具体实现类是PhoneWindow,而PhoneWindow的根本是DecorView,DecorView则是ViewGroup的子类,点击查看关于Android的组成。故此,我们通过观察这些类的关键代码入手。
Activity类dispatchTouchEvent(MotionEvent ev)方法:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent();
    }else {
        consume = child.dispatchTouchEvent(ev);
    }
    
    return consume;
}

现在贴上精简后的ViewGroup类onInterceptTouchEvent()源码:

       @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            ...
            intercepted = onInterceptTouchEvent(ev);
            ...
        }
        ...
        if (!canceled && !intercepted) {
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            ...
            }
            ...
        }
        ...
        return handled;
    }
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        ...
        return handled;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {}

以上便是对源码的事件分发机制的分析,使用的都是系统默认提供的ViewGroup(ConstraintLayout)、View(AppCompatButton),正常情况(无拦截事件)下的分发流程,确实符合前面我们分析的情况。下面我们通过简单的自定义View来继续分析。

通过自定义View分析事件分发的拦截逻辑

这里我们进行简单的自定义ViewGroup+自定义View来分析拦截和不处理(消费)事件。
简单的自定义ViewGroup:

public class MyLinearLayout extends LinearLayout {
    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean isTouch = true;
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            System.out.println("ViewGroup===onTouchEvent--"+isTouch);
        }
        return isTouch;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean isIntercept = false;
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            System.out.println("ViewGroup===onInterceptTouchEvent--" + isIntercept);
        }
        return isIntercept;
    }
}

简单的自定义View:

public class MyAppCompatButton extends AppCompatButton {
    public MyAppCompatButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean isTouch=true;
        if(event.getAction()==MotionEvent.ACTION_DOWN){
            System.out.println("View===onTouchEvent--"+isTouch);
        }
        return isTouch;
    }
}

-重构了onTouchEvent(MotionEvent event)方法,使用isTouch变量控制是否处理事件
具体在xml中的使用:

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <com.futurenavi.demo1.views.MyLinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            tools:ignore="MissingConstraints">

            <com.futurenavi.demo1.views.MyLinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                tools:ignore="MissingConstraints">

                <com.futurenavi.demo1.views.MyAppCompatButton
                    android:id="@+id/my_bt1"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="button1~"
                    tools:ignore="MissingConstraints" />
            </com.futurenavi.demo1.views.MyLinearLayout>
        </com.futurenavi.demo1.views.MyLinearLayout>

        <com.futurenavi.demo1.views.MyLinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            tools:ignore="MissingConstraints">

            <com.futurenavi.demo1.views.MyAppCompatButton
                android:id="@+id/my_btn2"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="button2~"
                tools:ignore="MissingConstraints" />
        </com.futurenavi.demo1.views.MyLinearLayout>

        <com.futurenavi.demo1.views.MyAppCompatButton
            android:id="@+id/my_btn3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="button3~"
            tools:ignore="MissingConstraints" />
    </LinearLayout>

在布局文件中应用了3组布局

最终的Acitvity页面

public class ActivityG extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_g);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            System.out.println("Activity===onTouchEvent");
            System.out.println("========================");
        }
        return super.onTouchEvent(event);
    }
}

下面是不同情况下的运行情况:
ViewGroup默认不拦截,View处理事件时,依次点击三个按钮的打印记录:

ViewGroup默认不拦截,View处理事件时

ViewGroup进行拦截,ViewGroup自身响应事件,View不处理事件时,依次点击三个按钮的打印记录:

此时需要把MyLinearLayout类下的boolean isIntercept = false;变更为true,MyAppCompatButton类下的boolean isTouch=true;变更为false

ViewGroup进行拦截,ViewGroup自身响应事件,View不处理事件时

ViewGroup进行拦截,ViewGroup自身不处理事件,依次点击三个按钮的打印记录:

此时需要把MyLinearLayout类下的boolean isTouch=true;变更为false

ViewGroup进行拦截,ViewGroup自身不处理事件

到这里,印证了前面的介绍以及经典图上所绘制的流程即事件会按照顺序一层一层进行分发,每一层都会都会判断是否拦截(ViewGroup),如果不拦截则继续往下,反之则查看当前ViewGroup是否消费事件,若最终找不到有能力消费的,则返回到Activity形成闭环。

当然,截止到目前,还有一点没有说到,即在ViewGroup拦截事件之后,后续是如何处理的呢?下面来看看关键源码:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        ...
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            ...
            intercepted = onInterceptTouchEvent(ev);
            ...
        }
        ...
         if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
        }
        ...
        return handled;
    }
  private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        ...
        return handled;
    }
public class MyLinearLayout extends LinearLayout {
    ...
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean isTouch = true;
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            System.out.println("ViewGroup===onTouchEvent--"+isTouch);
        }
        return isTouch;
    }
    ...
}

这里再提一提,如果想要更好的理解源码是如何进行事件分发的,最好通过DEBUG跟踪事件分的逻辑,

这里发现android-29和android-28、android-26略有差异,但不影响本篇内容事件分发机制的,看到文章的朋友可以试试。
另外,在ViewGroup类的执行dispatchTouchEvent()事件分发,对child进行判断的时候,可以看到系统源码中并不是简单的Activity-PhoneWindow(Window)-DecorView-我们的布局Layout-具体的View,而是:

  DecorView@ebcfe75[ActivityG]
  android.widget.LinearLayout{93999f3 V.E...... ........ 0,0-1440,2712}
  android.widget.FrameLayout{17cada3 V.E...... ........ 0,84-1440,2712}
  androidx.appcompat.widget.ActionBarOverlayLayout{ea7fb39 V.E...... ........ 0,0-1440,2628 #7f070056 app:id/decor_content_parent}
  androidx.appcompat.widget.ContentFrameLayout{32e548 V.E...... ........ 0,196-1440,2628 #1020002 android:id/content}
  androidx.constraintlayout.widget.ConstraintLayout{d004903 V.E...... ........ 0,0-1440,2432}
  android.widget.LinearLayout{11a8c3f V.E...... ........ 0,0-1440,2432}
  com.futurenavi.demo1.views.MyLinearLayout{9d95de9 V.E...... ........ 0,168-1440,336}
...

待续

总结



结合前面的三个关注点:每个Activity包含一个Window,而Window其实是一个PhoneWindow,每个PhoneWindow则包含一个DecorView,而每个DecorVIew都是一个Framelayout(也就是一个ViewGroup)。
也就是说,在开发者层面,顶层窗口是Window,顶层布局是DecorView(FrameLayout )

在Activity中的onCreate()方法第一时间会去执行了setContentView方法,即设置页面UI(布局),前面说到ViewGroup是Android中的布局的父类:

Activity的组成结构 View类及其子类
Activity的组成结构 View类及其子类(ViewGroup及其子类(布局类))

场景/疑问/好奇

讲讲 Android 的事件分发机制
查看过源码中的事件拦截方法吗?或者说在进行事件分发的时候如何让正常的分发方式进行拦截?
在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样?

上面的问题无论是面试还是平时开发都是很常见/常用的,各位看官心里是否或清晰或模糊或好奇呢?

在Android中,通常以一个Activity作为一个页面,在Activity的组成情况中我们知道每个Activity包含一个Window,而Window其实是一个PhoneWindow,每个PhoneWindow则包含一个DecorView,而每个DecorVIew都是一个Framelayout。

而事件,总是有一个触发点的,对于手机的事件通常从用户按下开始的,事件分发对应着三个重要方法:

上面三个方法,从字面就非常好理解,分别是事件分发、事件处理、事件拦截,在Android中无论AppCompatActivity、FragmentActivity都是Activity的子类,

举个不恰当的列子,Activity接到了一个“水幕电影”的项目,然后扭头就转包给了ViewGroup,这个ViewGroup是没有能力搞这东西的,但是又想赚钱就接了,然后又转包给了另外一个ViewGroup2,正所谓人以类聚,这ViewGroup2也是不会搞想赚钱的主,最后这个ViewGroup2把这活给了一个专业对口的View,这个View漂亮的完成了项目,引起了一片好评。

总结一下,Activity是第一时间接收到点击事件的,然后会分发给顶层ViewGroup,顶层ViewGroup会去检查是否拦截,不拦截则继续往下传递,若UI复杂则会重复该步骤,直到事件分发到具体View进行事件处理。看起来似乎也是挺简单明的,那么,通过源码来看看,Android具体在这如何进行三大环节中进行事件分发的。

DecorView@ebcfe75[ActivityG]
android.widget.LinearLayout{93999f3 V.E...... ........ 0,0-1440,2712}
android.widget.FrameLayout{17cada3 V.E...... ........ 0,84-1440,2712}
androidx.appcompat.widget.ActionBarOverlayLayout{ea7fb39 V.E...... ........ 0,0-1440,2628 #7f070056 app:id/decor_content_parent}
androidx.appcompat.widget.ContentFrameLayout{32e548 V.E...... ........ 0,196-1440,2628 #1020002 android:id/content}
androidx.constraintlayout.widget.ConstraintLayout{d004903 V.E...... ........ 0,0-1440,2432}
android.widget.LinearLayout{11a8c3f V.E...... ........ 0,0-1440,2432}
com.futurenavi.demo1.views.MyLinearLayout{9d95de9 V.E...... ........ 0,168-1440,336}
上一篇 下一篇

猜你喜欢

热点阅读