在了解View的事件分发之前,先了解下Activity的层级结构,便于更好的理解事件的传递顺序。
层级结构图
事件分发就是对MotionEvent事件进行分发的过程,即当一个MotionEvent产生后,系统需要把这个事件传递(处理)给一个具体的View,这个过程就是分发过程。
方法 | 作用 | 调用时刻 | 返回值 |
---|---|---|---|
dispathchTouchEvent | 进行事件的分发 | 事件传递给当前View时调用 | 是否消耗当前事件 |
onInterceptTouchEvent | 判断是否拦截某个事件 | 在ViewGroup的dispatchTouchEvent()内部调用 | 表示是否拦截当前事件 |
onTouchEvent | 处理点击事件 | 在dispathchTouchEvent内部调用 | 表示是否消耗当前事件 |
三者之间的关系用伪代码表示
public boolen dispatchTouchEvent(){
boolen consume = false;
if(onInterceptTouchEvent){
consume = onTouchEvent(ev); //如果被拦截,调用当前viewGroup的onTouchEvent
}else{
consume = child.dispatchTouchEvent(); //如果未被拦截,调用当前的子view的dispatchTouchEvent,即事件传递给子view
}
return consume;
}
事件是用户与屏幕发生交互时产生的,而Activity则是Android中负责与用户发生交互的组件。所以事件的传递,首先是到达Activity,再通过内部传递之后,到达我们的布局文件中的layout和View。事件发生之后,需要进行响应处理,再传递的过程中,由上往下,都有可能有机会处理一个事件序列。如果从Activity往下,到最终的View,事件都没有得到处理,则事件又从下往上,回到Activity,如果回到Activity之后,Activity没有处理这个事件,那么这个事件就会自动结束。
上面的讲解都是针对的ACTION_DOWN,ACTION_MOVE和ACTION_UP和ACTION_DOWN在传递过程中和ACTION_DOWN并不相同。简单来说,只有前一个事件返回了true时,才会收到ACTION_MOVE和ACTION_UP的事件。并且而最终会将ACTION_MOVE和ACTION_UP分发到消费到ACTION_DOWN的View手中。在分发的过程中,ACTION_MOVE和ACTION_UP与ACTION_DOWN分发的路线可能不回完全相同。
例如:
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
总结:如果在同一个事件序列里面,如果ACTION.DOWN事件不被这个View做出消耗,则后面陆续的事件序列则不会传递到这个View来
我们分别创建RelativeLayoutA、RelativeLayoutB,都继承自RelativeLayout,也等同于是ViewGroup,再创建一个MyView继承自Button类,也等同于是继承View。
在RelativeLayoutA类,RelativeLayoutB类中重写上面提到的三个方法,分别打印出他们的方法名,在MyView类中重写dispatchTouchEvent方法和onTouchEvent方法,打印他们的方法名。
//RelativeLayoutA的代码,RelativeLayoutB和MyView与这类似,这里不展示
public class RelativeLayoutA extends RelativeLayout {
private String TAG = "RelativeLayoutA";
public RelativeLayoutA(Context context) {
super(context);
}
public RelativeLayoutA(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RelativeLayoutA(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent: ");
return super.onInterceptTouchEvent(ev);
}
}
布局文件:
在RelativeLayoutA中嵌套RelativeLayoutB,RelativeLayoutB中嵌套MyView
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.three_viewtext.RelativeLayoutA
android:layout_width="300dp"
android:layout_height="300dp"
android:id="@+id/RelativeLayoutA"
android:layout_centerInParent="true"
android:background="#ffff00">
<com.example.three_viewtext.RelativeLayoutB
android:layout_width="150dp"
android:layout_height="150dp"
android:id="@+id/RelativeLayoutB"
android:layout_centerInParent="true"
android:background="#00ff00">
<com.example.three_viewtext.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/Button"
android:text="按钮"
android:layout_centerInParent="true"
/>
com.example.three_viewtext.RelativeLayoutB>
com.example.three_viewtext.RelativeLayoutA>
RelativeLayout>
第一种情况,直接运行程序点击按钮,打印日志如下:
在点击按钮时的事件列是:DOWN->IP,如上图,上半部分为DOWN的事件调用顺序,下半部分为UP的事件调用顺序。
因为Button默认是可以点击的(即使我们并没有设置点击监听事件),所以MyView打印出了onTouchEvent,随后返回了true,这个ACTION.DOWN事件就被MyView消耗掉了。
第二种情况, 把MyView的onTouchEvent事件返回false,编译运行后点击中间的按钮,再看下打印:
可以看到,从Activity->view事件都没有得到处理,则事件又从下往上,回到Activity,log只是打印出了ACTION.DOWN的打印,并没有像上面的log一样打印出ACTION.UP。UP事件没有传递到ViewGroup和View中
在这个Log里面由于MyView不处理事件,而RelativeLayoutB和RelativeLayoutA其实也是不处理自己事件的,最后交由了更高级别的ViewGroup(Activity)去响应了,所以后面的ACTION.UP不会再传递到这几个控件上来了。
在RelativeLayoutB中让onInterceptTouchEvent返回true,表示RelativeLayoutB会拦截事件自己处理,不分发给下一级View树处理,编译运行后点击中间的按钮,我们再来看看Log:从这个Log中可以看出,MyView并没有打印出来,说明他没有接收到事件,因为RelativeLayoutB已经把事件给拦截了,就不再分发给MyView,而RelativeLayoutB把事件拦截了后自己调用onTouchEvent,默认是没有消耗事件的,所以才会再调用RelativeLayoutB的onTouchEvent方法。同样,和2一样,UP事件没有传递到ViewGroup和View中。
注意事件拦截和事件消费是两回事,事件拦截说的是不把事件发给下一级View,而事件消费说的是处理完这个事件还要不要让上一级也处理,如果消费了事件那么就不会再让上一级处理这个事件。
setOnTouchListener方法,通过设置监听后可以在触摸的时候回调onTouch方法
在刚才的demo中,设置View的onTouch、onClick监听,,这里只做一个总结:
事件分发其实包含了三部分的事件分发,即:
当一个点击操作发生时,事件最先是传递给当前的Activity,由Activity的dispatchTouchEvent进行事件委派
//Activity的dispatchTouchEvent源码
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); //空方法
}
//委派给DecorView
if (getWindow().superDispatchTouchEvent(ev)) {
//如果下层处理了事件,返回true结束
return true;
}
//事件没有得到处理,调用Activity的onTouchEvent
return onTouchEvent(ev);
}
/*
getWindow()获得是Window的抽象类,而window的唯一实现就PhoneWindow,下面是PhoneWindow.superDispatchTouchEvent源码
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
//Decor即是DecorView,所以phoneWindow将事件传递给了DecorView
return mDecor.superDispatchTouchEvent(event);
}
/*
DecorView.superDispatchTouchEvent源码
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
//等于开始调用ViewGroup的dispatchTouchEvent,即到此为止,
//事件从Activity传到了ViewGroup
}
/*
Activity.onTouchEvent
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
这里只展示其中一些关键代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
final boolean intercepted;// 检查是否要拦截
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
/*
FLAG_DISALLOW_INTERCEPT设置后,ViewGroup无法栏除ACTION_DOWN之外的其他点击直接。
原因:在ViewGroup分发事件时,如果是ACTION_DOWN,会重置这个标志位
设置方法: requestDisallowInterceptTouchEvent
*/
if (!disallowIntercept) {
// 只有允许拦截才执行onInterceptTouchEvent方法
intercepted = onInterceptTouchEvent(ev);//调用onInterceptTouchEvent方法
ev.setAction(action); // restore action in case it was changed
} else {
//不允许拦截,直接设为false
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
/*
在这种情况下,actionMasked != ACTION_DOWN && mFirstTouchTarget == null
说明没有一个子View要去处理ACTION_DOWN事件,导致mFirstTouchTarget还是空的,
没有指向要处理事件的子View,所以接下来的其他事件,都不再继续分发下去了,而且拦截了事件让自己处理。
*/
intercepted = true;
}
//源码2662行
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
FLAG_DISALLOW_INTERCEPT是一个标记位,通过 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 可以设置它,一般是用在子View里面。如果FLAG_DISALLOW_INTERCEPT被设置了后,ViewGroup就无法拦截ACTION_DOWN以外的其他事件,这是因为ACTION_DOWN事件会重置FLAG_DISALLOW_INTERCEPT标记位
//源码(2650行),
if (actionMasked == MotionEvent.ACTION_DOWN) {
//down事件,做重置状态的操作,
cancelAndClearTouchTargets(ev);
resetTouchState(); //会对FLAG_DISALLOW_INTERCEPT进行重置
}
首先cancelAndClearTouchTargets方法会遍历清除所有的target,导致mFirstTouchTarget=null,然后在调用resetTouchState,重置触摸状态
resetTouchState的源码:
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
//重置标志位
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
分析到这里可得出如下结论:
3. ViewGroup不拦截事件时,事件会向下分发交给他的子View进行处理
//源码2722行
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--){
final int childIndex=getAndVerifyPreorderedIndex(
childrenCount,i,customOrder);
final View child=getAndVerifyPreorderedView(
preorderedList,children,childIndex);
/*
判断子元素是否能接收到点击事件,两个衡量标准
child.canReceivePointerEvents():是否在播动画
isTransformedTouchPointInView()点击事件的坐标是否落在子元素区域内
*/
if(!child.canReceivePointerEvents()
||!isTransformedTouchPointInView(x,y,child,null)){
continue;
}
newTouchTarget=getTouchTarget(child);// 查找child对应的TouchTarget
if(newTouchTarget!=null){
// 比如在同一个child上按下了多跟手指
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
//子View已经在自己的范围内得到了触摸。
//除了它正在处理的那个,给它一个新的指针。
newTouchTarget.pointerIdBits|=idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//dispatchTransformedTouchEvent()实际上调用子元素的dispatchTouchEvent
if(dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)){
// Child wants to receive touch within its bounds.
mLastTouchDownTime=ev.getDownTime();
if(preorderedList!=null){
// childIndex points into presorted list, find original index
for(int j=0;j<childrenCount; j++){
if(children[childIndex]==mChildren[j]){
mLastTouchDownIndex=j;
break;
}
}
}else{
mLastTouchDownIndex=childIndex;
}
mLastTouchDownX=ev.getX();
mLastTouchDownY=ev.getY();
// 如果处理掉了的话,将此child添加到touch链的头部
// 会更新 mFirstTouchTarget
newTouchTarget=addTouchTarget(child,idBitsToAssign);
alreadyDispatchedToNewTouchTarget=true;// 记录ACTION_DOWN事件已经被处理了。
break;
}
}
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//首先判断是否设置OnTouchListener,如果OnTouchListener中的onTouch方法中返回true,那么onTouchEvent就不会被调用,
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;// 如果被onTouch处理了,则直接返回true
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
View的事件传递及分发机制
Android之View篇2————View的事件分发
Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(上)
Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(中)
Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(下)