美文网首页Android开发学习和思考程序员程序员首页投稿
Android的事件分发机制(上)|SquirrelNote

Android的事件分发机制(上)|SquirrelNote

作者: 跳动的松鼠 | 来源:发表于2017-10-29 01:14 被阅读22次

    系列文章:
    Android的事件分发机制(上)|SquirrelNote
    Android的事件分发机制(下)|SquirrelNote

    前言

    了解并熟悉事件分发机制有助于更好的分析各种滑动冲突、滑动失效问题,更好地去扩展控件的事件功能和开发自定义控件。
    本篇将描述什么是事件分发,以及为什么会有事件分发机制,事件分发机制的过程。
    下一篇将从源码角度对事件分发机制进行解析。

    准备知识

    MotionEvent
    当手指接触屏幕后所产生的事件类型有以下几种:

    • ACTION_DOWN:手指按下
    • ACTION_MOVE:手指移动
    • ACTION_UP:手指离开

    一般情况下,一次手指触摸屏幕的行为会触发一系列点击事件,如下:

    • 点击屏幕后松开:DOWN-->UP
    • 点击屏幕滑动后再松开:DOWN-->MOVE-->...-->MOVE-->UP

    通过MotionEvent对象,可以获取点击事件发生的x和y坐标。系统提供了两组方法:getX/getY和getRawX和getRawY。它们的区别:getX/getY相当于当前View左上角的x和y的坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y的坐标。

    下面详细分析事件分发机制。

    什么是事件分发机制(本质)

    所谓点击事件的事件分发,其实就是对点击事件的分发过程,即当一个点击事件产生以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。

    为什么会有事件分发机制

    当我们点击的地方有多个View都可以响应的时候,那么这个点击事件应该给谁?为了解决这个问题,就有了事件分发机制。


    image.png

    如图:当点击View的时候,View和ViewGroup都能响应这个点击事件,为了确定是谁处理这个点击事件,就有了事件分发机制。

    三个关键方法

    点击事件的分发过程是由三个方法来完成的:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,想要弄清楚事件分发机制就是要弄清楚这三个方法。

    public boolean dispatchTouchEvent(MotionEvent ev)
    用来进行事件的分发。View/ViewGroup接收到触控事件最先调起的就是这个方法,然后在这个方法中判断是否处理拦截或者将事件分发给子元素

    public boolean onInterceptTouchEvent(MotionEvent ev)
    用来进行事件的拦截。ViewGroup中调用(View中没有此方法),在这个方法中判断是将事件交给ViewGroup处理或者将它传递给子元素,一般在这个方法中处理事件冲突。

    public boolean onTouchEvent(MotionEvent event)
    用来进行事件的处理。最后每个事件都会在这里被处理。

    那么,这三个方法到底有什么区别呢?它们是什么关系呢?其实它们的关系可以用如下伪代码表示:

       @Override
        public boolean dispatchTouchEvent(MotionEvent ev){
            //默认为false,不消费当前事件
            boolean consume=false;
            if(onInterceptTouchEvent(ev)){
                //调用onTouchEvent(ev)方法,处理事件
                consume=onTouchEvent(ev);
            }else{
                //调用子元素的dispatchTouchEvent(ev)方法,进行事件分发
                consume=child.dispatchTouchEvent(ev);
            }
        }
    

    下面一个demo,通过打印log具体分析事件分发的过程:

    image.png image.png

    activity_main.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="com.eventdispatchdemo.activity.MainActivity">
    
        <com.eventdispatchdemo.view.MyViewGroup1
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:background="@android:color/holo_blue_dark"
            tools:context="com.eventdispatchdemo.activity.MainActivity">
    
            <com.eventdispatchdemo.view.MyViewGroup2
                android:layout_width="220dp"
                android:layout_height="220dp"
                android:background="@android:color/holo_red_light">
    
                <com.eventdispatchdemo.view.MyView
                    android:layout_width="150dp"
                    android:layout_height="150dp"
                    android:background="@android:color/black"/>
            </com.eventdispatchdemo.view.MyViewGroup2>
    
        </com.eventdispatchdemo.view.MyViewGroup1>
        <com.eventdispatchdemo.view.MyButton
            android:id="@+id/btn_main"
            android:text="按钮"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    

    MainActivity.java

    public class MainActivity extends Activity {
    
        private MyButton mBtn;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            //初始化
            mBtn = (MyButton) findViewById(R.id.btn_main);
    
            //按钮的触摸事件
            mBtn.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            Log.e("TAG2", "MyButton:onTouch---DOWN");
                            break;
                        case MotionEvent.ACTION_MOVE:
                            Log.e("TAG2", "MyButton:onTouch---MOVE");
                            break;
                        case MotionEvent.ACTION_UP:
                            Log.e("TAG2", "MyButton:onTouch---UP");
                            break;
                    }
                    /**
                     * 这里返回false,MyButton方法里面的onTouchEvent方法将会被调用,onClick方法也会被调用
                     * 如果这里如果返回true,MyButton方法里面的onTouchEvent方法将不会被调用,onClick方法也不会被调用
                     * 说明优先级:OnTouchListener>onTouchEvent>onClickListener,即onClickListener处于事件
                     * 传递的尾端
                     */
                    return false;
                }
            });
            //按钮的点击事件
            mBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Log.e("TAG2", "MyButton:onClick");
                }
            });
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Log.e("TAG2","Activity---dispatchTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e("TAG2","Activity---dispatchTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("TAG2", "Activity---dispatchTouchEvent---UP");
                    break;
                default:
                    break;
            }
            return super.dispatchTouchEvent(event);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Log.e("TAG2", "Activity---onTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e("TAG2", "Activity---onTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("TAG2", "Activity---onTouchEvent---UP");
                    break;
                default:
                    break;
            }
            return super.onTouchEvent(event);
        }
    }
    

    MyViewGroup1.java

    /**
     * 创建者     yangyanfei
     * 创建时间   2017/10/28 下午 10:01
     * 作用         自定义ViewGroup
     * 重写三个构造方法
     * 重写dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent
     * <p/>
     * 版本       $$Rev$$
     * 更新者     $$Author$$
     * 更新时间   $$Date$$
     * 更新描述   ${TODO}
     */
    public class MyViewGroup1 extends LinearLayout {
        public MyViewGroup1(Context context) {
            super(context);
        }
    
        public MyViewGroup1(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public MyViewGroup1(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            Log.e("TAG","MyViewGroup1:dispatchTouchEvent执行");
            return super.dispatchTouchEvent(ev);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            Log.e("TAG","MyViewGroup1:onInterceptTouchEvent执行");
            return super.onInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.e("TAG","MyViewGroup1:onTouchEvent执行");
            return super.onTouchEvent(event);
        }
    
        //伪代码
        /*@Override
        public boolean dispatchTouchEvent(MotionEvent ev){
            //默认为false,不消费当前事件
            boolean consume=false;
            if(onInterceptTouchEvent(ev)){
                //调用onTouchEvent(ev)方法,处理事件
                consume=onTouchEvent(ev);
            }else{
                //调用子容器的dispatchTouchEvent(ev)方法,进行事件分发
                consume=child.dispatchTouchEvent(ev);
            }
        }*/
    
    }
    

    说明:MyViewGroup1和MyViewGroup2代码一样。

    MyView.java

    /**
     * 创建者     yangyanfei
     * 创建时间   2017/10/28 下午 09:57
     * 作用         自定义View
     * 重写三个构造方法
     * 重写dispatchTouchEvent和onTouchEvent方法
     * 说明:在View里面没有onInterceptEvent这个方法
     * <p/>
     * 版本       $$Rev$$
     * 更新者     $$Author$$
     * 更新时间   $$Date$$
     * 更新描述   ${TODO}
     */
    public class MyView extends View {
        public MyView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        public MyView(Context context) {
            super(context);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            Log.e("TAG","MyView:dispatchTouchEvent执行");
            return super.dispatchTouchEvent(event);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.e("TAG","MyView:onTouchEvent");
            return super.onTouchEvent(event);
        }
    }
    

    MyButton.java

    /**
     * 创建者     yangyanfei
     * 创建时间   2017/10/29 上午 12:04
     * 作用         自定义Button
     * 重写dispatchTouchEvent方法和onTouchEvent方法,并获取了MotionEvent各个事件状态,打印输出了每一个状态下的信息。
     * <p/>
     * 版本       $$Rev$$
     * 更新者     $$Author$$
     * 更新时间   $$Date$$
     * 更新描述   ${TODO}
     */
    public class MyButton extends Button {
        public MyButton(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.e("TAG2","MyButton:dispatchTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e("TAG2","MyButton:dispatchTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("TAG2","MyButton:dispatchTouchEvent---UP");
                    break;
            }
            return super.dispatchTouchEvent(event);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
    //                Toast.makeText(getContext(),"MyButton---onTouchEvent---Down",Toast.LENGTH_SHORT).show();
                    Log.e("TAG2","MyButton:onTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e("TAG2","MyButton:onTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("TAG2","MyButton:onTouchEvent---UP");
                    break;
            }
            return super.onTouchEvent(event);
        }
    }
    

    完整demo,请移步到我的GitHub进行下载:https://github.com/FlyStorm/EventDispatchDemo

    当点击MyView控件,即黑色区域的时候,打印log,如下:

    image.png

    从日志输出结果可以看到:嵌套的MyView,事件传递的顺序是:Activity-->MyViewGroup1-->MyViewGroup2-->MyView,Android中事件传递是从ViewGroup传递到View,这里最终传递给MyView,MyView没有处理事件,又会回传给MyViewGroup2,到MyViewGroup1,回到Activity处理事件。

    当点击下方Button按钮的时候,打印log如下:

    image.png

    从日志输出结果可以看到:onTouch事件要先于onClick执行,onTouch是在事件分发方法dispatchTouchEvent中调用,而onClick是在事件处理方法onTouchEvent中被调用,onTouchEvent要后于dispatchTouchEvent方法调用。

    事件分发的过程:
    对于一个ViewGroup,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent方法就会被调用,如果ViewGroup的onInterceptTouchEvent方法返回true就代表它要拦截事件,事件就会交给此ViewGroup处理,即它的onTouchEvent方法就会被调用;如果ViewGroup的onInterceptTouchEvent方法返回fase就代表它不拦截事件,当前事件就会继续传递给它的子元素,然后子元素的dispatchTouchEvent方法就会被调用,这样反复直到事件被最终处理。

    当一个View需要处理事件时,如果它设置了OnTouchListener,那么它里面的onTouch方法将会被回调。如果onTouch返回false,当前View的onTouchEvent方法将会被调用,onClick方法也会被调用;如果返回true,onTouchEvent方法将不会被调用,onClick方法也不会被调用,说明优先级:OnTouchListener>onTouchEvent>onClickListener,即onClickListener处于事件传递的尾端。

    打个比方:假如这个点击事件是一个难题,这个难题交给上级领导,上级领导把这个难题 分给员工去处理(这个就是事件分发),员工可以搞定,处理完这个难题,(onTouchEvent返回true)。假如员工搞不定,(onTouchEvent返回了false),难题一定要解决,那只能交给上级领导解决(上级领导的onTouchEvent被调用),如果上级领导再搞不定,那再交给上级的上级去解决,就这样将难题一层一层地往上抛。

    以上是根据我的一些理解,做的总结分享,旨在抛砖引玉,希望有更多的志同道合的朋友一起讨论学习,共同进步!

    相关文章

      网友评论

        本文标题:Android的事件分发机制(上)|SquirrelNote

        本文链接:https://www.haomeiwen.com/subject/gqcjpxtx.html