Android事件分发机制(一)
2017-08-29 15:29
302 查看
当手指触摸屏幕后会产生一系列的事件(如点击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,用下面的伪代码表示。(理解下面的伪代码很重要,解释了事件分发机制的原理)
如果View是不可点击的(mClickable == false),则onTouchEvent()方法返回false,否则返回true.当可点击时,在手指抬起时,如果识别到是单击手势(按下马上抬起)则回调OnClickListener.onClick()
对于View:
在非容器类型的View中,dispatchTouchEvent()方法里面首先会回调TouchListener.onTouch()方法,如果该方法消费了事件返回true,则dispatchTouchEven()结束并返回true,onTouchEvent()方法则不会被调用。
对于ViewGroup:
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中的方法注释。
可见,触摸事件会优先分发到Activity的dispatchTouchEvent()中,然后通过调用 getWindow.dispatchTouchEvent()将事件继续分发到 DecorView中,再一步步分发到其他控件,如下图所示。
当一个事件产生后,传递顺序如下:Activity->Window->View,按照分发机制去分发事件。
现在我们用事实说话,写个简单的例子,验证上面所说的。
TouchActivity.java
MyLayout.java
activity_touch.xml
运行界面:
点击BUTTON1时的输出Log:
点击BUTTON2时的输出Log:
还记得一开始说的吗?当我们点击一个View的时候,手势事件并不是直接传给该View,而是经过了事件分发才传递到该View。当点击按钮时,触摸事件首先传给Activity,经过一系列的分发,最后才传到点击的按钮上。如果在这分发的过程中,事件被拦截了,点击的按钮就没有反应。前面所说的就是剖析这个分发的过程是如何进行的。
BUTTON2不同于BUTTON1的地方是,BUTTON2在OnTouchListener里面拦截了事件,按照最上面说的伪代码,将导致onTouchEvent()方法不被执行,间接导致OnClickListener没被回调。
然后我们再看看点击BUTTON3时的输出Log:
BUTTON3不同于BUTTON1的地方是,BUTTON3设置为不可点击。这里需要再说明的是,当一个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么后续的同一事件序列中的其他事件都不会再分发给它去处理,而是交给它的父容器去处理。因此我们可以从Log看到,当触摸事件ACTION_DOWN开始时,会分发给所有的控件,如果都没有消费该事件,则后续的ACTION_UP直接交给了最顶层的Activity去处理,这样Activity的onTouchEvetn就能执行到了。
好的,终于写完了!!!事件分发机制的原理也就这么一回事了。接下来,我会再写一篇文章,从源码分析事件分发机制,也仅是再进一步验证上面所说的,以及加深下印象罢了。然后再稍微讲一下,根据事件分发机制如何去处理滑动冲突问题,相当于对所学知识的一个应用吧。
在细说事件分发机制之前,先说下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,经过一系列的分发,最后才传到点击的按钮上。如果在这分发的过程中,事件被拦截了,点击的按钮就没有反应。前面所说的就是剖析这个分发的过程是如何进行的。
BUTTON2不同于BUTTON1的地方是,BUTTON2在OnTouchListener里面拦截了事件,按照最上面说的伪代码,将导致onTouchEvent()方法不被执行,间接导致OnClickListener没被回调。
然后我们再看看点击BUTTON3时的输出Log:
BUTTON3不同于BUTTON1的地方是,BUTTON3设置为不可点击。这里需要再说明的是,当一个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么后续的同一事件序列中的其他事件都不会再分发给它去处理,而是交给它的父容器去处理。因此我们可以从Log看到,当触摸事件ACTION_DOWN开始时,会分发给所有的控件,如果都没有消费该事件,则后续的ACTION_UP直接交给了最顶层的Activity去处理,这样Activity的onTouchEvetn就能执行到了。
好的,终于写完了!!!事件分发机制的原理也就这么一回事了。接下来,我会再写一篇文章,从源码分析事件分发机制,也仅是再进一步验证上面所说的,以及加深下印象罢了。然后再稍微讲一下,根据事件分发机制如何去处理滑动冲突问题,相当于对所学知识的一个应用吧。
相关文章推荐
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下) .
- android 编程下 Touch 事件的分发和消费机制
- Android 触摸事件分发和拦截机制
- Android事件分发机制源码分析之View篇
- Android知识架构 · 电话面试 · Android事件分发机制
- 从源码角度解析Android事件分发机制
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android 编程下 Touch 事件的分发和消费机制
- Android MotionEvent事件分发机制源码剖析
- Android中View的事件分发机制
- Android事件分发机制
- 《Android深入透析》之Android事件分发机制
- Android之Touch 事件的分发和消费机制
- android事件分发机制dispatch
- android 事件分发机制(图文详解)
- 【Android】【Framework】Android事件分发(传递)机制
- Android事件分发机制完全解析(终极版二)
- Android Touch事件的分发响应机制
- Android43_Touch事件的分发和消费机制