老规矩,看源码一定要带着问题、推测或结论去看,不能看到太深,要能刹得车~不然会陷在源码中。像我这样的小菜鸟,总是在看过源码之后才知道Android源码设计的强大,所以在此总结一下源码中是如何处理事件分发的。
注意:本文中所有源码分析部分均基于 API25 版本,由于安卓系统源码改变很多,可能与之前版本有所不同,但基本流程都是一致的。
本文将对ViewGroup的dispatchTouchEvent()做了一个比较全面的注释,并形成了简化后的伪代码辅助理解。
单个View分析
首先我们需要先引发一个问题。需要自定义一个View,重载构造方法,并在Activity中实现两个接口,然后来看一下他都点击处理情况.
button= (WidgetButton) findViewById(R.id.btn_content);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG,"事件");
}
});
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(TAG,"Touch");
return false;
}
});
当我们点击这个自定义控件时
I/widget: Touch
I/widget: Touch
I/widget: Touch
I/widget: 事件
但是如果我们将OnTouchListener中的OnTouch返回true
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(TAG,"Touch");
return true;
}
});
点击控件的结果是
I/widget: Touch
I/widget: Touch
I/widget: Touch
好了问题出现
为什么setOnTouchListener中的onTouch返回true后OnClick就不执行了呢?我们点进去看setOnTouchListener和setOnClickListener做了什么?
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
这里发现将监听赋给了mOnTouchListener和mOnClickListener,我们暂时记住这两个变量名。
我们要知道当我们点击自定义View控件时,首先会调用父类(View)的dispatchTouchEvent方法。然后看一下这里边几句重要的代码。
9999 boolean result = false;
...
10017 if (li != null && li.mOnTouchListener != null
10018 && (mViewFlags & ENABLED_MASK) == ENABLED
10019 && li.mOnTouchListener.onTouch(this, event)) {
10020 result = true;
10021 }
10023 if (!result && onTouchEvent(event)) {
result = true;
10025 }
这里我们可以看到之前记住的那个变量名mOnTouchListener,这里判断如果它的onTouch返回true的时候result赋值为ture。(在源码中如果有些变量不太明白干什么的,千万不要纠结,这里判断的这几个条件都会满足)继续往下看,如过result为false,因为用这里用的是&&所以在第一个条件未满足的情况下是不会调用第二的条件的,但是重点就在这个第二个条件中(onTouchEvent方法)。
11185 case MotionEvent.ACTION_UP:
...
11216 performClick();
在performClick方法的5637行会调用 li.mOnClickListener.onClick(this); 是不是之前赋值的那个变量名。所以这里可以知道,当mOnTouchListener.onTouch()为true时就不会调用onTouchEvent()方法,但是mOnClickListener.onClick()在onTouchEvent() - up事件 - performClick()方法中,所以也不会调用。
Activity - ViewGroup 分析
为什么要从Activity说起呢。事件收集之后最先传递给 Activity, 然后依次向下传递。
首先自定义ViewGroup,我这里继承的是RelativeLayout因为它也是继承自ViewGroup并且没有对触摸事件进行处理,然后重载构造方法,并重写三个与触摸事件有关的方法
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG," WidgetViewGroup onTouchEvent "+event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(TAG," WidgetViewGroup onInterceptTouchEvent "+ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG," WidgetViewGroup dispatchTouchEvent"+ ev.getAction());
return super.dispatchTouchEvent(ev);
}
在MainActivity中重写两个与触摸事件有关的方法,为什么是两个呢,因为在Activity中没有Intercept事件,因为没有意义,如果拦截了会导致点击什么效果都没有。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG," MainActivity dispatchTouchEvent "+ ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG," MainActivity onTouchEvent "+ event.getAction());
return super.onTouchEvent(event);
}
点击屏幕时输出如下 这里0表示Down 2表示Move 1表示Up
I/widget: MainActivity dispatchTouchEvent 0
I/widget: WidgetViewGroup dispatchTouchEvent0
I/widget: WidgetViewGroup onInterceptTouchEvent 0
I/widget: WidgetViewGroup onTouchEvent 0
I/widget: MainActivity onTouchEvent 0
I/widget: MainActivity dispatchTouchEvent 2
I/widget: MainActivity onTouchEvent 2
I/widget: MainActivity dispatchTouchEvent 1
I/widget: MainActivity onTouchEvent 1
这样就知道了触摸事件之间执行顺序的情况。
好了问题出现
为什么会这样调用呢?那么先从Activity的dispatchTouchEvent看起。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();//这个方法是个空方法,是给用户进行重写的
}
//这里说明如果这个判断返回true就不执行Activity的onTouchEvent()方法
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
这里有简单的注释,那判断中的方法是什么呢?进去看一下。
public abstract boolean superDispatchTouchEvent(MotionEvent event);
可以发现他是Window抽象类的一个抽象方法,在文件开始的注释中说明了Window抽象类仅有一个实现类PhoneWindow,那么就得去PhoneWidow找这个方法的实现。
PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
这里调用的DecorView的方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
这里调用的父类的方法,但是父类FrameLayout并没有这个方法,只好再去FrameLayout的类ViewGroup中去找。最终在ViewGroup中找到了相应的方法(dispatchTouchEvent),并做了很多操作,那么我们看一下做了什么操作。2145行开始
//判断是否有触摸设备 比如触摸笔一类的
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
//这是一个赋值功能 AccessibilityService 可以不用手指进行点击 比如抢红包
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {//事件安全检查
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/**
* 第一步:对于ACTION_DOWN进行处理(Handle an initial down)
* 因为ACTION_DOWN是一系列事件的开端,当是ACTION_DOWN时进行一些初始化操作.
* 从源码的注释也可以看出来:清除以往的Touch状态(state)开始新的手势(gesture)
* cancelAndClearTouchTargets(ev)中有一个非常重要的操作:
* 将mFirstTouchTarget设置为null!!!!
* 随后在resetTouchState()中重置Touch状态标识
* */
// Handle an initial down.
//Down为事件的开始,所以这里判断是不是事件的开始
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
//这个方法用于清理标志,里边有一个TouchTarget判读是否是第一次触发Down,如果是什么都不处理,如果不是将一些标志全部置为初始化
cancelAndClearTouchTargets(ev);
//重置所有接触状态,准备一个新的触摸循环
resetTouchState();
}
/**
* 第二步:检查是否要拦截(Check for interception)
* 在dispatchTouchEvent(MotionEventev)这段代码中
* 使用变量intercepted来标记ViewGroup是否拦截Touch事件的传递.
* 该变量在后续代码中起着很重要的作用.
*
* 拦截 intercepted =true
*/
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//正因为这个||所以在move事件是也能够拦截
//这里的 (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0是否为true取决于 requestDisallowInterceptTouchEvent(?)
// 这个方法是用于是否允许父类进行拦截 true 为不让父控件拦截。这里个人理解为判断子View是否有调用这个方法。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//这里的拦截并没有返回 只是将intercepted 设为 实现方法中返回的值 标示是否做拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
/**
* 第三步:检查cancel(Check for cancelation)
*
*/
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
/**
* 第四步:事件分发(Update list of touch targets for pointer down, if needed)
*/
//不是ACTION_CANCEL并且ViewGroup的拦截标志位intercepted为false(不拦截)
//intercepted 未被拦截 如果拦截了会跳过这里边的方法
if (!canceled && !intercepted) {
//之前说的那个赋值功能
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//判断是否是Down事件 还有一个 多手指的判断
//第二次Move事件时不会执行这里的代码 所以不会遍历子控件,由于move事件频繁调用 这是对move事件的一个优化
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
// 移除所有的多手指操作
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
//如果当前含有子控件
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
//重排序 按找Z来排序
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
//是否是用户自己排序
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//事件分发 倒序 遍历 重排序后的集合 后添加的先接收
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//拿到第i个View 相当于 preorderedList.get(childIndex )
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
//赋值功能
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//这里判断这个View能不能够接收事件
//clickable Invisiable 点击事件不在 view范围内(通过pointInView) 正在动画
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
/**
* 执行到了下面
* child 绝对会接受到事件
*/
//如果只分析Down操作 这里返回空 如果是move它就不为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.
//如果能够找到接收事件的target直接break
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
/**
* 真正做事件分发
* child 不为空
*
*
* 如果子类 onTouch 返回true (根据子类onTouchEvent 来返回)
*/
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添加到Target 并赋给mFirstTouchTarget 做为将要接收move事件的view
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();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
//如果该ViewGroup拦截事件为true 那么mFirstTouchTarget 为null (会跳过上面的方法直接到这)
//如果没有拦截并且点击锁定子view那么mFilrstTouchTarget不为null
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
/**
* 真正做事件分发
* child 为空
*/
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;
//如果第三个参数为null将会调用ViewGroup的onTouchEvent(),如果不为null将不会调用ViewGroup的onTouchEvent()调用的是子View的onTouchEvent()
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;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
这就是整个dispatchTouchEvent()的代码,重要的都做了注释。还有一部分重要的代码是
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
}
之前一直提到的真正的事件分布,传view和不传view 的情况,如过view为Null调用ViewGroup的super.dispatchTouchEvent(),相当于调用ViewGroup的onTouchEvent(),如果不为null这调用子view的dispatchTouchEvent()。
经过源码的分析,可以简化一个伪源码,如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (!onInterceptTouchEvent(ev)) {//如果没有拦截
//之前会判断是否存在这个 符合要求的child
//这个返回结果相当于子View的onTouchEvent的返回结果
if(child.dispatchTouchEvent(transformedEvent)){
//target是锁定事件的那个view之后的move事件就发生在它身上 mFirstTouchTarget 默认为null
mFirstTouchTarget = target;
}
}
//这个判断相当于没有拦截或不存在符合条件的子view
if(mFirstTouchTarget == null){
handled = onTouchEvent();//调用自身的onTouchEvent();
}else{
handled = true;
}
return handled;
}
通过源码可以直接的了解出 Activity - ViewGroup - View 的一个事件传递情况。这样也就解释了上面的问题(个人理解,如果是ViewGroup - ViewGroup 将会是一个递归的情况)
总结
- 事件分发中用到了责任链模式,上层View可以拦截事件自己处理,也可以发布给子View,如果子View处理不了还可以返回到上层View进行处理,既保证了事件的有序性,又非常的灵活。
- View 的 dispatchTouchEvent 主要用于调度自身的监听器和 onTouchEvent。
- View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
- 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
- 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
- ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。
- 只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。
- (源码理解)ViewGroup的dispatchTouchEvent 方法处理了所有触摸操作,onInterceptTouchEvent和onTouchEvent这两个方法并没有返回结果,只是返回true、false告诉dispatchTouchEvent应该做啥。
这篇文章是在我学习的基础上进行了总结,可想而知我还是个很小的菜鸟,如果其中有错误还请指出,我会尽快修改文章,并改正自己的理解,谢谢。
网友评论