Android事件分发机制(一)

当手指触摸屏幕后会产生一系列的事件(如点击DOWN、移动MOVE、抬起UP等),事件的信息记录在MotionEvent(手势事件)对象中。这里说的事件分发机制,其实指的是MotionEvent的分发过程。当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View(ViewGroup继承自View),这个传递的过程就是事件分发过程。在这里先明确的说一下,当我们点击一个View的时候,手势事件并不是直接传给该View,而是经过了事件分发才传递到该View。


在细说事件分发机制之前,先说下View和ViewGroup关系,方便后续理解。Android的UI界面都是由View和ViewGroup,及他们的派生类组合而成的。其中,View是所有组件的基类,也就说ViewGroup本身也继承自View(所以,View包含了ViewGroup)。ViewGroup是容纳其他组件的容器。常用的布局RelativeLayout、LinearLayout、FrameLayout等都是继承父类ViewGroup来实现的。


好的,现在真正开始分析事件分发机制了。事件分发的过程由三个重要的方法共同完成:

  • public boolean dispatchTouchEvent(MotionEvent event)
处理事件的分发,所有View都有该方法。返回结果表示是否消耗当前事件。(下面两个方法都在dispatchTouchEvent方法里面被调用
  • public boolean onInterceptTouchEvent(MotionEvent event)
用于拦截事件,只有ViewGroup才有该方法。在dispatchTouchEvent()方法中调用。返回结果表示是否拦截当前事件。主要作用是ViewGroup向其子控件分发手势事件之前,对相关事件进行拦截。 如果ViewGroup拦截了某个事件,那么在同一个事件序列(从手指按下到抬起发生的事件为一个事件序列)当中,此方法不会被再次调用。
  • public boolean onTouchEvent(MotionEvent event)
处理手势事件,所有View都有该方法。在dispatchTouchEvent()方法中调用。返回结果表示是否消耗当前事件。


然后再介绍下跟触摸事件相关的其他两个常用方法:
  • public void setOnTouchListener(OnTouchListener listener)
触摸事件传到当前的View时,回调该监听器。
  • public void setOnClickListener(OnClickListener listener)
当前的View被单击时,回调该监听器。(手指按下马上抬起为一个单击手势)

重点来了!上述几个方法的关系可以区分View和ViewGroup,用下面的伪代码表示。(理解下面的伪代码很重要,解释了事件分发机制的原理

public boolean onTouchEvent(MotionEvent event) {
        if (mDisable) { // 不可用则返会mClickable
            return mClickable;
        }
        if (mClickable) {
            if (event.getAction() == MotionEvent.ACTION_UP && 识别到为单击事件) {
                mOnClickListenr.onClick(this);
            }
            return true; // 可点击的状态下一定返回true
        }
        return false; // 不可点击的状态下一定返回false
    }

如果View是不可点击的(mClickable == false),则onTouchEvent()方法返回false,否则返回true.当可点击时,在手指抬起时,如果识别到是单击手势(按下马上抬起)则回调OnClickListener.onClick()

对于View
public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {  // 通过调用setOnTouchListener()设置mOnTouchListener
        return true;  
    }
    return onTouchEvent(event); // 当识别到到单击手势时,OnClickListener在这里被回调(此时的动作为ACTION_UP)

} 

在非容器类型的View中,dispatchTouchEvent()方法里面首先会回调TouchListener.onTouch()方法,如果该方法消费了事件返回true,则dispatchTouchEven()结束并返回true,onTouchEvent()方法则不会被调用。

对于ViewGroup
    private View mTargetView = null; // 消费了ACTION_DOWN的目标控件
    private boolean mDisallowIntercept = false; // 控制不允许容器拦截事件,默认为允许

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) { // ACTION_DOWN事件
            mTargetView = null; // 一个事件序列开始,重置为空
            if (!mDisallowIntercept  // 允许拦截。这里对mDisallowIntecept取反,当它的值为false时取反后条件才为真
                    && onInterceptTouchEvent(ev)) { // 当前容器判断是否拦截
                return super.dispatchTouchEvent(ev); // 调用上面View的dispatchTouchEvent()
            } else {
                for (int i = 0; i < childrens; i++) { // 把事件逐个分发给包含当前手势事件坐标的子控件
                    child = childrens[i];
                    if (child.dispatchTouchEvent(ev)) { // 子控件又递归调用dispathTouchEvent方法;如果事件被消费则停止向下分发
                        mTargetView = child; // 找到消费了事件的目标控件
                        return true;
                    }
                }
                return super.dispatchTouchEvent(ev); // // 调用上面的View.dispatchTouchEvent()
            }

        } else { // 其他事件
            if (mTargetView == null) { // 说明ACTION_DOWN没有被消费或者中途事件被拦截了,则直接交由父容器处理
                return super.dispatchTouchEvent(ev);
            } else {
                if (!mDisallowIntercept // 允许拦截。这里对mDisallowIntecept取反,当它的值为false时取反后条件才为真
                        && onInterceptTouchEvent(ev)) { // 当前容器继续判断是否拦截
                    ev.setAction(MotionEvent.ACTION_CANCEL); // 通知目标控件事件被拦截了
                    mHasIntercepted = true; // 已拦截
                    mTargetView.dispatchTouchEvent(ev);
                    mTargetView = null; // 将目标控件置空,后续事件交由当前容器自己出来
                    return true;
                } else { // 不拦截,事件继续交给目标控件
                    return mTargetView.dispatchTouchEvent(ev);
                }
            }
            
            // 手指抬起
            if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_MOVE) {
                mDisallowIntercept = false; // 手指抬起时,重置为false,父容器下一次事件开始时又可以拦截事件了
            }
        }
    }


dispatchTouchEvent()是事件分发的关键,其他方法都在这里被直接或间接调用。当一个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么后续的同一事件序列(从这次按下到手指抬起发生的一序列事件)中的其他事件都不会再分发给它去处理,而是交给它的父容器去处理。即如果当前容器下发的ACTION_DOWN的事件没有一个子控件去消费(此时mTargetView == null),则后续的事件则不会继续分发,直接由当前容器自己处理。如果子控件消费了事件,则后面容器仍有权限去拦截事件(通过onInterceptTouchEvent()方法判断是否拦截),不下发给子控件。可见,所有传到子控件的事件,都会经过它的父容器。

如果子控件不想父容器拦截事件,在收到ACTION_DOWN事件时调用父容器的requestDisallowInterceptTouchEvent(true)每次一个事件序列开始前mDisallowIntercept都被重置为false了,所以必须在ACTION_DOWN中通知父容器不要拦截事件。这里并不是让onInterceptTouchEvent()方法返回false,而是让onInterceptTouchEvent()根本不被执行。代码的第7行和第25号,if(条件一 && 条件二 ),这里使用了逻辑运算符&&,“与”的作用,如果条件一为false,则条件二就完全不用去判断,if直接为false。这就能解释了,当mDisallowIntercept == true 时,也就是!mDisallowIntercept ==  false,onInterceptTouchEvent()就没有被执行的原因。

总之,MotionEvent是通过dispatchTouchEvent()方法一层层地分发下去,如果事件中途被消费,则停止分发。

在开发中,通常都是在Activity的onCreate()中调用setContentView(R.layout.custom_layout)来实现想要的页面布局。页面都是依附在窗口Window之上的,而DecorView即是窗口最顶层的视图。DecorView本身也继承FrameLayout,它里面的布局如下所示。



我们调用setContentView(),就是就是把我们的布局放在id为content的FrameLayout容器里,这也是为什么这个方法叫setContentView,而不是setView或其他名字啦。

而Activity中有两个方法 dispatchTouchEvent(MotionEvent ev)、onTouchEvent(MotionEvent event),它们俩的关系跟前面介绍的一样。下面是Activity中的方法注释。

/**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
// getWindow()返回的是PhoneWindow的实例,查看PhoneWindow的代码,其实这里调用的是DecorView.dispatchTouchEvent()
        if (getWindow().superDispatchTouchEvent(ev)) { 
            return true;
        }
        return onTouchEvent(ev);
    }


可见,触摸事件会优先分发到Activity的dispatchTouchEvent()中,然后通过调用 getWindow.dispatchTouchEvent()将事件继续分发到 DecorView中,再一步步分发到其他控件,如下图所示。




当一个事件产生后,传递顺序如下:Activity->Window->View,按照分发机制去分发事件。


现在我们用事实说话,写个简单的例子,验证上面所说的。


TouchActivity.java


public class TouchEventActivity extends Activity {

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

        // button1
        View button1 = findViewById(R.id.button1);
        button1.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i("Test", "button1::OnTouchListener");
                return false;
            }
        });
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("Test", "button1::OnClickListener");
            }
        });

        // button2
        View button2 = findViewById(R.id.button2);
        button2.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i("Test", "button2::OnTouchListener");
                return true; // 这里返回true,消费当前事件
            }
        });
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("Test", "button2::OnClickListener");
            }
        });

        // button3
        View button3 = findViewById(R.id.button3);
        button3.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i("Test", "button3::OnTouchListener");
                return false;
            }
        });
        button3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("Test", "button3::OnClickListener");
            }
        });
        button3.setClickable(false);  // button3设置为不可点击!!!!!!
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("Test", "Activity::dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("Test", "Activity::onTouchEvent");
        return super.onTouchEvent(event);
    }
}




MyLayout.java

public class MyLayout extends LinearLayout {

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("Test", "MyLayout::onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("Test", "MyLayout::dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("Test", "MyLayout::onTouchEvent");
        return super.onTouchEvent(event);
    }
}



activity_touch.xml

<?xml version="1.0" encoding="utf-8"?>
<com.example.huangziwei.myapplication.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                               android:layout_width="match_parent"
                                               android:layout_height="match_parent"
                                               android:orientation="vertical">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button1"
        />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button2"
        />

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button3"
        />
</com.example.huangziwei.myapplication.MyLayout>


运行界面:



点击BUTTON1时的输出Log:


点击BUTTON2时的输出Log:



还记得一开始说的吗?当我们点击一个View的时候,手势事件并不是直接传给该View,而是经过了事件分发才传递到该View。当点击按钮时,触摸事件首先传给Activity的dispatchTouchEvent方法,再由内部的Window把事件传递给DecorView,经过一系列的分发,最后才传到点击的按钮上。如果在这分发的过程中,事件被拦截了,点击的按钮就没有反应。前面所说的就是剖析这个分发的过程是如何进行的。

BUTTON2不同于BUTTON1的地方是,BUTTON2在OnTouchListener里面拦截了事件,按照最上面说的伪代码,将导致onTouchEvent()方法不被执行,间接导致OnClickListener没被回调。

然后我们再看看点击BUTTON3时的输出Log:


BUTTON3不同于BUTTON1的地方是,BUTTON3设置为不可点击。这里需要再说明的是,当一个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么后续的同一事件序列中的其他事件都不会再分发给它去处理,而是交给它的父容器去处理。因此我们可以从Log看到,当触摸事件ACTION_DOWN开始时,会分发给所有的控件,如果都没有消费该事件,则后续的ACTION_UP直接交给了最顶层的Activity去处理,这样Activity的onTouchEvetn就能执行到了。


好的,终于写完了!!!事件分发机制的原理也就这么一回事了。接下来,我会再写一篇文章,从源码分析事件分发机制,也仅是再进一步验证上面所说的,以及加深下印象罢了。然后再稍微讲一下,根据事件分发机制如何去处理滑动冲突问题,相当于对所学知识的一个应用吧。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android 事件分发机制是指在用户与Android设备进行交互时,Android系统如何接收并分发这些事件的过程。事件分发机制包括三个阶段:分发、拦截和处理。 1. 分发阶段:事件Android设备的底层硬件驱动程序开始,通过InputEvent分发给View层。在View层中,事件分为两类:MotionEvent和KeyEvent。MotionEvent表示触摸事件,包括按下、移动、抬起等操作;KeyEvent表示按键事件,包括按下和抬起。 2. 拦截阶段:在事件分发到View层后,会从最上层的View开始进行事件分发,直到有View对事件进行拦截。如果有View对事件进行了拦截,则事件不会继续向下分发,而是由该View进行处理。View是否拦截事件的判断由onInterceptTouchEvent方法完成,如果该方法返回true则表示拦截事件。 3. 处理阶段:如果事件没有被拦截,则会被传递到最底层的View进行处理。在View中,事件处理由onTouchEvent方法完成。如果该方法返回true,则表示事件已经被处理,不再需要继续向下分发;如果返回false,则会继续向上分发直到有View对事件进行拦截。 Android事件分发机制的流程如下: ![image.png](attachment:image.png) 需要注意的是,事件分发机制是一个逆向分发的过程,即从底层向上分发,而不是从顶层向下分发。这是因为底层的View需要先处理事件,如果底层的View不拦截事件事件才能向上分发
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值