在讲解事件传播之前,我们需要先了解一下我们的布局在屏幕中究竟是怎么显示的:
我们在写好布局文件之后,需要在 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
开始,中间夹杂 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)的组件
ACTION_DOWN
开始,中间夹杂若干 ACTION_MOVE
,以 ACTION_UP
结束。ACTION_DOWN
,则该事件序列的后续其他事件单元也不会再传递给该组件。android:clickable
和 android:longClickable
属性也影响着事件的消费,二者有一个为 true,该 View 则可以消费事件,android:longClickable
默认 false,而 android:clickable
分情况,例如 Button 默认为 true,TextView 默认为 false。