前言 :我们在Android开发过程中经常会遇到多个View,ViewGroup嵌套的问题,例如: ViewPager中嵌套Fragment,而在Fragment中需要实现一个横向滚动的广告位,这个时候,就会遇到广告栏的滑动事件和Viewpager的滑动事件冲突的问题,想要快速解决这种问题,我们需要对View的事件传递机制有较为深刻的理解.
接下来会介绍Activity,View,ViewGroup三者的触摸事件传递机制.一次完整的事件主要包括三个阶段,分别是事件的分发,拦截和消费.
目录
1.触摸事件的类型
触摸事件对应的是MotionEvent类,事件的类型主要有下面三种:
- ACTION_DOWN :用户手指按下的操作,一次按下标志着一次触摸事件的开始
- ACTION_MOVE : 用户的手指按下之后,在松开手指之前,如果手指一动的距离超过一定的距离,就会被判定为ACTION_MOVE操作,一般情况下,手指轻微的一动就会是触发一系列的移动事件
- ACTION_UP :用户手指离开屏幕,一起离开的操作标志着一次触摸事件的结束
在单次屏幕操作的过程中,DOWN和UP是必须的,但是MOVE是可以不存在的,如果单机一下就松开,那么只会检测到按下和抬起事件
2.事件传递的三个阶段
- 分发 Dispatch :事件分发对应着dispatchTouchEvent方法,在Android系统中,所有的触摸事件都是通过这个方法来分发的->方法的原型如下 :
public boolean dispatchTouchEvent(MotionEvent ev)
在这个方法中,根据当前的视图的具体实现逻辑,来决定是否消费这个事件还是继续将事件分发给子视图处理,方法的返回值代表是否消费事件,返回true表示消费,不再继续分发下去,返回super.dispatchTouchEvent表示继续分发下去.如果当前的视图是ViewGroup以及子类,则会调用onInterceptTouchEvent方法判断是否拦截事件.
- 拦截 Intercept : 事件的拦截对应着onInterceptTouchEvent方法,这个方法只是在ViewGroup和其子类中才会有,在View和Activity中是不存在的 . 方法的原型如下 :
public boolean onInterceptTouchEvent(MotionEvent ev)
同理,这个方法也是根据Booelan值来决定是否拦截事件的,返回true表示拦截这个事件,不继续分发,交给自己的OnTouchEvent方法处理.返回false或者是super.onInterceptTouchEvent表示不拦截,交给子视图继续处理.
- 消费 Consume : 事件的消费对应着onTouchEvent方法,方法原型如下 :
public boolean onTouchEvent(MotionEvent ev )
该方法返回值为true表示当前视图可以处理对应的事件,事件不会向上传递给父视图,返回false表示当前视图不处理这个事件,事件会继续传递给父视图的onTouchEvent方法进行处理
在Android系统中,拥有事件传递处理能力的类有下面三种 :
- Activity :拥有dispatchTouchEvent和onTouchEvent两个方法
- ViewGroup : :拥有dispatchTouchEvent , onInterceptTouchEvent和onTouchEvent三个方法
- View :拥有dispatchTouchEvent和onTouchEvent两个方法
3.View的事件传递机制
虽然ViewGroup是View的子类,但是这里所说的View是指除了ViewGroup的View控件,例如TextView,Button,CheckBox,ImageView等,view控件本身已经是最小的单位,不能再作为其他控件的View容器.View拥有dispatchTouchEvent和onTouchEvent两个方法.我们通过自定义一个TextView来了解一下事件的传递过程:
public class MyTextView extends TextView {
private static final String TAG = "MyTextView";
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 分发
* @param event
* @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"dispatchTouchEvent->手指按下事件");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"dispatchTouchEvent->移动事件");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"dispatchTouchEvent->手指松开事件");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onTouchEvent->手指按下事件");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"onTouchEvent->移动事件");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"onTouchEvent->手指松开事件");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG ,"onTouchEvent->事件取消");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
写一个activity测试:
class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnTouchListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/*
设置TextView的点击事件和触摸事件 Kotlin代码
*/
mMyTextView.setOnClickListener(this)
mMyTextView.setOnTouchListener(this)
}
val TAG: String = "MainActivity"
/**
* View的点击事件
*/
override fun onClick(v: View?) {
when (v?.id) {
R.id.mMyTextView -> Log.e(TAG, "View的点击事件")
}
}
/**
* View的触摸事件
*/
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (v?.id) {
R.id.mMyTextView -> {
when (event?.action) {
MotionEvent.ACTION_DOWN -> Log.e(TAG, "onTouch->按下")
MotionEvent.ACTION_MOVE -> Log.e(TAG, "onTouch->移动")
MotionEvent.ACTION_UP -> Log.e(TAG, "onTouch->抬起")
}
}
}
return super.onTouchEvent(event)
}
/*
接下来是Activity的两个方法
*/
/**
* 分发
* @param event
* @return
*/
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> Log.e(TAG, "dispatchTouchEvent->手指按下事件")
MotionEvent.ACTION_MOVE -> Log.e(TAG, "dispatchTouchEvent->移动事件")
MotionEvent.ACTION_UP -> Log.e(TAG, "dispatchTouchEvent->手指松开事件")
}
return super.dispatchTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> Log.e(TAG, "onTouchEvent->手指按下事件")
MotionEvent.ACTION_MOVE -> Log.e(TAG, "onTouchEvent->移动事件")
MotionEvent.ACTION_UP -> Log.e(TAG, "onTouchEvent->手指松开事件")
MotionEvent.ACTION_CANCEL -> Log.e(TAG, "onTouchEvent->事件取消")
}
return super.onTouchEvent(event)
}
}
运行代码 ,点击TextView,看下日志 :
E/MainActivity: dispatchTouchEvent->手指按下事件
E/MyTextView: dispatchTouchEvent->手指按下事件
E/MainActivity: onTouch->按下
E/MyTextView: onTouchEvent->手指按下事件
E/MainActivity: dispatchTouchEvent->手指松开事件
E/MyTextView: dispatchTouchEvent->手指松开事件
E/MainActivity: onTouch->抬起
E/MyTextView: onTouchEvent->手指松开事件
E/MainActivity: View的点击事件
上面的代码中,dispatchTouchEvent和onTouchEvent这两个方法的返回值可能存在下面的3种情况 :
- false
- true
- super的同名方法
不同的返回值会导致事件传递流程的结果不一样,通过控制返回值不断测试,得到:
(手画的太烂,将就着还是可以看的)
从上面的流程图得出:
- 触摸事件的传递流程是从dispatchTouchEvent开始的,如果默认返回的是父类的同名函数,则事件将会依照嵌套层次从外向内传递,到达最内层的时候,就由他的onTouchEvent方法处理,该方法如果能处理,就返回true,如果处理不了就返回false.这是事件向外层传递,由他的onTouchEvent方法处理,依次类推.
- 如果事件在向内传递的过程中,dispatchTouchEvent返回true,则会导致改事件被提前消费掉,不会向内层传递 ,内层的View将不会收到这个事件
- View事件触发顺序是先执行onTouch方法,在最后才执行onClick方法.如果onTouch方法返回true,则事件不会传递,最后也不会调用onClick方法,如果onTouch返回false,则事件继续传递
4.ViewGroup的事件传递机制
viewGroup是作为View的容器存在的(常见的四大布局控件,列表listView,RecyclerView ,等等 ...) ,ViewGroup拥有dispatchTouchEvent , onInterceptTouchEvent和onTouchEvent三个方法 ,比View多了一个onInterceptTouchEvent方法.我们自定义一个相对布局看看
class MyRelativelayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
val TAG: String = "MyRelativelayout"
/**
* 分发
* @param event
* @return
*/
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> Log.e(TAG, "dispatchTouchEvent->手指按下事件")
MotionEvent.ACTION_MOVE -> Log.e(TAG, "dispatchTouchEvent->移动事件")
MotionEvent.ACTION_UP -> Log.e(TAG, "dispatchTouchEvent->手指松开事件")
}
return super.dispatchTouchEvent(event)
}
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> Log.e(TAG, "onInterceptTouchEvent->手指按下事件")
MotionEvent.ACTION_MOVE -> Log.e(TAG, "onInterceptTouchEvent->移动事件")
MotionEvent.ACTION_UP -> Log.e(TAG, "onInterceptTouchEvent->手指松开事件")
}
return super.onInterceptTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> Log.e(TAG, "onTouchEvent->手指按下事件")
MotionEvent.ACTION_MOVE -> Log.e(TAG, "onTouchEvent->移动事件")
MotionEvent.ACTION_UP -> Log.e(TAG, "onTouchEvent->手指松开事件")
MotionEvent.ACTION_CANCEL -> Log.e(TAG, "onTouchEvent->事件取消")
}
return super.onTouchEvent(event)
}
修改上面的布局,让TextView处于这个自定义的布局中
点击,查看日志
E/MainActivity: dispatchTouchEvent->手指按下事件
E/MyRelativelayout: dispatchTouchEvent->手指按下事件
E/MyRelativelayout: onInterceptTouchEvent->手指按下事件
E/MyTextView: dispatchTouchEvent->手指按下事件
E/MainActivity: onTouch->按下
E/MyTextView: onTouchEvent->手指按下事件
E/MainActivity: dispatchTouchEvent->手指松开事件
E/MyRelativelayout: dispatchTouchEvent->手指松开事件
E/MyRelativelayout: onInterceptTouchEvent->手指松开事件
E/MyTextView: dispatchTouchEvent->手指松开事件
E/MainActivity: onTouch->抬起
E/MyTextView: onTouchEvent->手指松开事件
E/MainActivity: View的点击事件
可以看出,与View事件的流程唯一不一样的地方是MainActivity和TextView之间增加了一层MyRelativeLayout,通过控制返回值,得到 :
通过上面的流程图,我们可以得出结论 :
- 触摸事件的传递顺序: Activity ==>ViewGroup ==>子View
- ViewGroup通过onInterceptTouchEvent方法对事件进行拦截,如果方法返回true,则事件不会继续传递给子View,如果返回false或者父类的同名方法,则事件会继续传递给子View
- 在子View中对事件进行处理以后,ViewGroup将接受不到任何事件 ...
下篇: Android中View的绘制流程