Android View的事件分发
最近刚看完android-Ultra-Pull-To-Refresh下拉刷新的源码,发现在写自定义控件时,对于View的事件的传递总是搞不太清楚,而View事件的分发机制,又是解决可能出现的滑动冲突的基础,因此专门学习了一下,结合一个例子来好好分析分析。
先上Demo,很简单,就是一个自定义ViewGroup和一个自定义View。View分发机制中,有三个很重要的方法是:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,它们分别对应着事件的分发、拦截和消耗,因此我在自定义View的这三个方法里加上Log,根据打印的日志来分析下具体的流程。
public class CustomViewGroup extends LinearLayout {
private static final String TAG = "Mylog";
public CustomViewGroup(Context context) {
super(context);
}
public CustomViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "customViewGroup.dispathcTouchEvent "+ev.toString());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "customViewGroup.onInterceptTouchEvent "+ev.toString());
return super.onInterceptTouchEvent(ev);
// return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "customViewGroup.onTouchEvent "+event.toString());
return super.onTouchEvent(event);
// return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
}
public class CustomView extends View {
private static final String TAG = "Mylog";
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "customview.dispatchtouchevent "+event.toString());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "customview.onTouchEvent "+event.toString());
// return true;
return super.onTouchEvent(event);
}
}
customView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "customView.onTouch "+event.toString());
return false;
}
});
customViewGroup.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "customViewGroup.onTouch "+event.toString());
return false;
}
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "Activity.dispatchTouchEvent "+ev.toString());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "Activity.onTouchEvent "+event.toString());
return super.onTouchEvent(event);
}
在Activity界面,我对CustomView和CustomViewGroup设定了onTouch监听。
下图是界面的一个示意图,Activity的界面包含了一个ViewGroup,而ViewGroup里包含了一个View,最简单的线性布局。
事件传递的基本原则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给自身,并调用其dispatchTouchEvent方法,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着调用onTouchEvent来处理这个事件;如果这个ViewGroup不拦截,则将事件传递给子元素,子元素调用其onInterceptTouchEvent方法,如此反复直到最终被处理。
同一事件序列:指手指接触屏幕起,到手指离开屏幕的那一刻,在这个过程中产生的一系列动作。一般以down事件开始,一系列move事件,并以一个up事件结束。
可以表示为:[down, move,move,move......up]
在上述的基本原则下,我们来根据例子分析下View的事件分发过程。
在默认情况下,点击区域3 View,Activity消耗事件
打印出来的Log如上,事件ACTION_DOWN先从Activity的dispatchTouchEvent开始传递,向下分发,由于Activity和ViewGroup默认都是不拦截事件的,因此不会拦截,一直传递到最内侧的View,注意到View的onTouch方法比其onTouchEvent方法先调用,这表示onTouch具有更高的优先级。由于我默认设置view也不处理事件,因此事件最终回到了Activity的onTouchEvent中,并接受了ACTION_UP事件。这表明一旦一个View接受了ACTION_DOWN事件,那么接下来的同一序列的事件都将交给它处理。结合下图可以更好的来理解。
将代码中,CustomView的onTouchEvent返回值为true,则表示可以对接受到的事件进行消耗。我们点击区域3,并滑动一段距离,此时打印log如下:
可以看到,当View的onTouchEvent返回true时,当ACTION_DOWN事件分发到View后,就不在会向父层转发,表示此View将会对事件经行处理,之后的ACTION_MOVE等同一序列的事件,都将沿着之前的路径转发给此View。
将CustomView的onTouchEvent返回改成super.onTouchEvent,并将CustomViewGroup中的onTouchEvent和onInterceptTouchEvent返回均设为true,表示将由ViewGroup来拦截事件,并进行处理。我们点击内部的区域3 View:得到的打印logo如下:
从log中,我们可以发现,一旦ViewGroup准备拦截事件,则事件将不再被转发给View,因此View的dispatchTouchEvent等函数均不会被触发,另外,在之后的同一序列事件中,ViewGroup的onInterceptTouchEvent函数也不会被触发,这表示:一旦某个ViewGroup决定拦截事件,则系统不再会询问接下来的事件ViewGroup是否要拦截。
这种情况比较简单,此时因为点击的区域中,不包含其他View,因此ViewGroup将不会把事件转发给ViewGroup中的其他子View。而在Activity和ViewGroup中正常的进行事件分发,这里给出打印的logo,流程图示意省略。
这种情况相对少见一点。我们需要将ViewGroup的onInterceptTouchEvent设为super,onTouchEvent设为true,View的onTouchEvent设为super. 这保证了事件将会被传递给子View,但子View不会去消耗这个事件。同样点击区域3 View后,logo如下:
和预期的一致,事件正常的分发到了View当中,但是子View并没有处理事件,而是交回到ViewGroup来处理。值得注意的是,之后同一序列的其他事件,都不在会分发给View,而是直接交给ViewGroup的onTouch方法去处理。这意味着一个View一旦接收了事件的转发而不处理,那么系统将不会把同一序列的其他事件转发给它。
大概情况就这么几种,常见的一般是直接由ViewGroup来消耗或者View消耗事件。
这里主要还是从应用层表面来分析了一些View事件的分发过程,如果想深入了解各个函数的实现,可以去阅读一下View的源码,从而更好的证实上述的结果。另外,注意到,View(不是ViewGroup)是不包含onInterceptTouchEvent方法的,这表示只要有事件传递给View,其onTouchEvent方法就会被触发,而ViewGroup是默认不拦截任何事件的,其onInterceptTouchEvent方法默认返回false。
在了解了View的事件分发机制后,就可以对一些出现滑动冲突的情况进行自己的处理,之后的文章中会继续写写学习心得。
本文来自网易实践者社区,经作者周龙授权发布。