android的事件分发机制是面试的时候必问的问题,并且在开发的过程正遇到的机遇 跟吃饭一样平常,四年前就知道了这个问题,但是让我说 我还是说不明白,所以还是写一下 没事可以翻出来看看,
那分发的对象是谁呢?也就是TouchEvent 其实就是点击事件,
TouchEvent 有一个down 一个up 不确定有几个的move事件,因为一次触摸按下和抬起手指只有一次,但是可以没有滑动 或者多个滑动
说个比较简单的例子,ViewGroupA嵌套ViewGroupB嵌套View,
那么事件传递顺序就是
ViewGroupA: dispatchTouchEvent
ViewGroupA: onInterceptTouchEvent
ViewGroupB: dispatchTouchEvent
ViewGroupB: onInterceptTouchEvent
View: dispatchTouchEvent
View: onTouchEvent
ViewGroupB onTouchEvent
ViewGroupA onTouchEvent
大致就是这样,View里面是没有onInterceptTouchEvent的,因为他不会有子类 分发给谁去啊,
直接看代码,先看dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 先判断是不是down事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
//如果是down事件,那么就证明是一个新的事件 所以把当前的事件置空或者回复初始值
cancelAndClearTouchTargets(ev);
//这一步将mFirstTouchTarget 属性置空
//mFirstTouchTarget 表示是否拦截了事件,拦截==null 不拦截!=null
resetTouchState();
}
//--------------------------------------------------------------------------------------------------
// 检查是否拦截
final boolean intercepted;
//也就是说在第一次点击的时候 或者 我已经拦截了这个事件的时候 才会走这个方法
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//disallowIntercept 表示不允许拦截
//FLAG_DISALLOW_INTERCEPT 表示禁止Viewgroup拦截除了down以外的事件 子view可以通过requestisallowInterceptD来设置
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//所以说 只要在down 或者不拦截事件 并且子view没设置不让父容器拦截的时候,才会走到这里
//调用本身的onInterruptTouchEvent方法
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
//没有触摸目标,这个动作不是初始下降。
//这个视图组继续拦截触摸。
intercepted = true;
}
//--------------------------------------------------------------------------------------------------
return handled;
}
接下来看onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
在满足众多条件后才返回true 一般都返回false ,也就是说 一般都不进行拦截,如果想拦截 你就重新这个方法 让他返回true;
我们继续看viewGroup的dispatchTouchEvent下面的代码
public boolean dispatchTouchEvent(MotionEvent ev) {
//这部分代码 上面已经进行分析了
//--------------------------------------------------------------------------------------------------------------
//获取子view数组
final View[] children = mChildren;
//倒叙遍历子view
for (int i = childrenCount - 1; i >= 0; i--) {
//我也不知道干啥的 名称的意思是 获取和验证预排序索引 个人感觉就是检测一下索引
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//看方法名的意思是 获取和验证预排序视图
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//如果有一个具有可访问性焦点的视图,我们希望它先得到事件,
// 如果没有处理,我们将执行正常的调度。
// 我们可以进行双重迭代,但在给定的时间范围内,这是比较安全的
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//如果子视图可以接收指针事件,则返回true
//如果子视图在转换到坐标空间时包含指定的点,则返回true。子项不能为空
//也就是说 看看这个view能不能处理点击事件 不能就算了
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//获取指定子视图的触摸目标,如果未找到则返回null
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
//我也不明白
//重置取消下一个标记
//如果先前设置了标志,则返回true
resetCancelNextUpFlag(child);
//判断是否有子view 有子view的话 就调用子view的dispatchTouchEvnet
//如果没有子view 则调用supre.disatchTouchEvent
//得到是否拦截 如果有拦截则走方法里的代码
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();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
来看一下dispatchTransformedTouchEvent
//如果当前view可以处理事件 则会走到这一步
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
//省略不必有的代码,其实就是看是否有子view 有点话就调用子view的dispatchTouchEvent
if (child == null) {
//没有的话 就调用父类的dispatchtouchEvent
handled = super.dispatchTouchEvent(event);
} else {
//
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
return handled;
}
如果child为null 就调用super.dispatchTouchEvent(event)其实是调用的View的dispatchTouchEvent(event)
如果child不为null的话 也分两种情况,如果child是ViewGroup的话 那就和咱们一开始分析的一样了
如果child是view的话 那就是走的View的dispatchTouchEvent(event)
所以我们看一下View的dispatchTouchEvent(event)
//View类
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
boolean result = false;
//又是那个什么什么过滤触摸事件以应用安全策略
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//onTouch默认是返回false的
result = true;
}
//注释2
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
View的dispatchTouchEvent(event)比较简单 因为不需要分发 只需要判断是否处理就好
重点看蓝色部分,
如果设置了OnTouchListener 则先执行onTouchListener的Touch方法,
如果Touch返回true,则将result设置为true,这样的话进不到注释2代码了,所以onTouchEvent也就失效了
如果正常情况下 会走onTouchEvent方法
所以Touch和onTouchEvent这才是真正处理事件的方法
好了 ,目前为止 事件的从上往下传递就结束了,但是这是最底层view是个view,并且消费了此次事件,
如果最底层不是view呢 ? 如果也是个viewGroup那么 事件是如何交给当前viewgroup处理的呢?
我们接着看Viewgroup的dispatch方法
public boolean dispatchTouchEvent(MotionEvent ev) {
//上面已经分析过了
//------------------------------------------------------------------
//上面已经把viewgroup遍历一遍了,如果有view消费的话 mFirstTouchTarget!=null;
//或者就是viewgroup拦截了这次事件 就不去遍历子view
// mFirstTouchTarget==null 证明没有view去消费这个事件
if (mFirstTouchTarget == null) {
// 重点看第三个参数 null 也就是child==null
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
return handled;
}
看蓝色部分,就是如果当前ViewGroup没有找到消费事件的view,或者是当前ViewGroup拦截了这次事件 就不会去遍历子view
则mFirstTouchTaefet==null,会走dispatchTransformedTouchEvent并且child为null,
我们在上面分析过 如果child为null,则调用自己父类的dispatch方法,也就是view的dispatch方法,从而进入onTouch方法
也就是调用了ViewGroup的onTouch方法,
这样的话 如果都没有处理事件,那么就完成了从下往上传递,
-----------------------------------------------------------------------------------------------------------------
那么还有一个问题,假如当前viewGoup拦截了这次事件,
就会调用自身的OnTouch方法,如果自身的onTouch返回false则还是会调用父控件的onTouche,
那如果自身onTouch返回ture,那么是如和不去调用父view的OnTouch方法的呢,
是因为如果ViewGroupA在遍历子view的时候 发现ViewGroup的dispatchTouch方法返回true,那么就将mFirstTouchTarget!=null,所以就dispatchTransformedTouchEvent的child就不为null,所以就进不去自己的OnTouch方法了
----------------------------------------------------------------------------------------------------------------
分发说完了 我们说一下工作中遇到的问题 怎么解决,
其实也没啥,重写view的onInterceptTouchEvent不就行了吗,想拦截就返回true不想拦截就返回false,
对 就是这样,但是有一点需要注意,判断拦截不拦截应该在TouchEvent的move里判断,因为如果在down中判断,一旦你拦截了那么 move和up就都给当前view处理了,所以在move里拦截 你有更多的选择,
那么还有个问题,就是比如listview里一个viewpage,当viewpage滑动到最后页面后,viewpage就不拦截事件了,讲事件交给父控件去处理,那这会怎么办,也就是说 我要在子控件中控制父控件拦截不拦截,
还记得一开始分析dispatch的时候的disallowIntercept属性吗?就是不允许拦截 只要在它为false的时候 父控件才可以拦截,
disallowIntercept是根据FLAG_DISALLOW_INTERCEPT来控制的,那么我们可不可以让子view通过设置这个属性控制父view拦截不拦截呢? 当然是可以了,那就是通过调用父控件的requestDisallowInterceptTouchEvent方法来控制FLAG_DISALLOW_INTERCEPT,间接的控制disallowIntercept,这样就达到我们想要的效果了
好了 完了