点击事件的分发,其实就是对MotionEvent事件的分发。
当事件产生后,系统会把这个事件传递到某个具体的View,这个传递的过程是由三个很重要的方法共同完成。
dispatchTouchEvent:
进行事件的分发,如果事件传递到了该View,那么此方法一定会被调用。
返回的结果受到当前View的onTouchEvent和下级View的onInterceptTouchEvent的影响。
onInterceptTouchEvent:
处理是否拦截某个事件,如果当前的View拦截了,那么同一个事件序列中,此方法不会被调用。返回结果表示是否拦截该事件。
同一个事件序列是指手指从按下到手指离开屏幕,在这个过程所产生的一系列事件。
DOWN事件开始,很多MOVE事件,UP事件结束。
onTouchEvent:
处理点击事件,返回的结果表示是否消耗当前的事件。如果不消耗,则在同一事件序列中,当前的View无法再次接收到事件。
结合上面的三个方法说一下大体的过程:
当点击事件产生了
事件最先传递给Activity,Activity再传递到Window,Window再传递到最顶层的View。
而最顶层的View接收到事件就会进行事件的分发。
顶层View,一般都是ViewGroup的dispatchTouchEvent调用,用于处理事件的分发,
如果onInterceptTouchEvent返回true,表示拦截此事件,那么该ViewGroup的onTouchEvent就会调用。
如果onInterceptTouchEvent返回false,那么表示它不拦截该事件。
这个事件就会传递到它的子元素,而子元素同样也会重复该过程。直到事件被最终处理。
一,Activity对事件分发的过程
Activity—->Window—–>最顶层的View
这个过程究竟是怎么样的?
点击事件产生了
首先Activity的dispatchTouchEvent进行事件的分发:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
(getWindow返回的是Window对象)
会把MotionEvent事件交给Window的superDispatchTouchEvent去处理。
查看Window的源码得知,Window是个抽象类,Window的实现类是PhoneWindow。
指定到PhoneWindow的superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
(mDecor是DecorView类型的变量)
PhoneWindow又会把事件传递到DecorView中。
而DecorView继承自FrameLayout。FrameLayout继承自View,DecorView它就是最顶层的View。
至此事件由Activity---->Window----->最顶层的View的过程就完成了。
现在写了一个十分简单的布局来理解上面所说的:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
style="@style/Botton_Common"
android:id="@+id/btn_next"
android:text="下一个"/>
</LinearLayout>
然后在Activity的setContenView中去加载这个layout。
它的层级关系是下面这样的:
DecorView正是位于最顶层,而我们布局的View,是它的子View。
可以通过
(ViewGroup)getWindow().getDecorView().findViewById(R.id.content).getChild(0)来获得布局的View。
二,顶层View对点击事件的分发
最顶层View的事件分发过程:
最顶层View一般都是一个ViewGroup,上面提到的DecorView就是ViewGroup。
最顶层View的dispatchTouchEvent调用负责事件分发,
onInterceptTouchEvent返回true的情况:
如果设置了setOnTouchListener(此时的mOnTouchListener不为null),就会调用onTouch方法。onTouch返回false,onTouchEvent将会被调用,返回true,onTouchEvent不会调用。
如果onTouchEvent中设置了setOnClickListener(此时的mClickListener不为null),那么onClick会被调用。
onInterceptTouchEvent返回false的情况:(不拦截)
则事件会传递到它所在点击事件链上的子View。
这时事件已经从顶层View传递到了子View中。
传递到子View会继续循环上面的过程。
最顶层View的dispatchTouchEvent
查看ViewGroup的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept =
(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
先看这个条件
(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)
MotionEvent.ACTION_DOWN表示的是DOWN事件。
当事件成功的由ViewGroup的子View处理后,mFirstTouchTarget会被赋值,不为null。
ViewGroup不拦截事件,交由ViewGroup的子View去处理后,mFirstTouchTarget!=null成立。
那么当一个MotionEvent.ACTION_DOWN和MotionEvent.ACTION_UP事件到来时,上面那个条件就不成立。
那么onInterceptTouchEvent将不会再被调用。
那如果只面对MotionEvent.ACTION_DOWN事件,那么ViewGroup的onInterceptTouchEvent会被调用,拦截。
一种特殊情况的考虑:
FLAG_DISALLOW_INTERCEPT标记位,它会在子View的requestDisallowInterceptTouchEvent方法中进行设置。
它一旦设置,ViewGroup将不拦截事件,但除了ACTION_DOWN事件。
为什么是除了ACTION_DOWN?
因为ACTION_DOWN会重置FLAG_DISALLOW_INTERCEPT标记位。在子View中设置是没有用处的。
if(actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();//重置FLAG_DISALLOW_INTERCEPT
}
一个结论:
当ViewGroup决定拦截事件,那么后续的点击事件都会交给它处理,并且不再调用它的onInterceptTouchEvent。
我的解释(可能有误):
当ViewGroup拦截了DOWN事件后,调用onInterceptTouchEvent去拦截处理,
那么后来的MOVE,UP事件过来时就不会通过
(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)这个判断。
那么表示后续的点击事件默认都会交给该ViewGroup去处理,并且不再调用onInterceptTouchEvent。
另一个结论:
在子View中可以通过requestDisallowInterceptTouchEvent方法
来设置FLAG_DISALLOW_INTERCEPT标记位。
来干预父View的事件分发处理,但是DOWN事件除外。
总结:
①onInterceptTouchEvent并不是每个事件都会被调用。如果我们想提前处理所有的点击事件,那么在
dispatchTouchEvent去处理。
(后面这个总结不太理解)
②可以利用子View来标记FLAG_DISALLOW_INTERCEPT来干扰父View的事件分发过程。
------未完待续......----------