文章目录
- Android-事件分发机制
- 开始
- 从View的点击事件开始吧
- ViewGroup的点击事件
- 拦截部分
- 接着看拦截之后的事情
- ViewGroup事件分发的总结
- 题外话,滑动事件冲突处理
Android-事件分发机制
- 注:参考郭神博客
- 本人在郭神的基础上做下总结,尽量以更简单的语法让你事件分发机制
- 相信我,认真读完,没有收获算我输
开始
从View的点击事件开始吧
- 还记得我们在代码中怎么为一个Button设置他的点击事件嘛?如下
findViewById(R.id.bt_test).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//做我们自己的事情
}
});
- 上面的方法很简单,我在这里不再解释了
- 但是,onClick方法只适合用来处理点击事件,如果我们想处理其他事件怎么办呢?Android为我们提供了另外一个方法
findViewById(R.id.bt_test).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
- 可以看到,这个方法与上面方法不同之处在于这个方法为我们把当前事件也传了进来,那这意味着我们是不是可以处理更多类型的事情呢?事实证明的确如此,我们只需要在onTouch方法里面判断事件的类型,然后按照需求处理即可,
- 分发给当前View的这一个时间序列全部经过这个方法
- 为什么这么说呢?,大家都知道,一个事件在分发给View或者ViewGroup的时候,会通过disPatchTouchEvent方法来处理,其中,View和ViewGroup对这个方法的实现细节是不一样的,普通的view间接或者直接的继承自View,所以被分发时直接使用View的dispatchTouchEvent方法来处理,
- 一般的ViewGroup是间接或者直接继承自VIewGroup,而ViewGroup是继承自View的,不过他对View的dispatchTouchEvent方法做了重写,所以View和ViewGroup对事件的处理逻辑是不同的,我们先来看普通的View是怎么处理的
- 来到View的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
- 所以,这里很清楚的看到,当onTouch方法也就是我们通过setOnTouchListener设置进去的监听方法返回true的话,onTouchEvent方法就得不到执行,而这个方法直接影响onClick方法。
- 这里我拿的是api = 28 的代码,可能不同版本的代码略有不同,这里我把好多代码删掉,留下有用的
- 我们只需要关心最中间的两个if判断句,先看第一个,第一个有三个条件,一个一个来
- 第一个条件是mListenerInfo参数不为null,点击源码我们看一下这个mListenerInfo参数
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
- 所以,当这个setOnTouchEvent方法被调用之后,这个参数就不会为空,且将我们在setOnTouchEvent方法里面传进去的对象赋值给mListenerInfo的mOnTouchListener 参数
- 看上面if语句第二个条件,这个条件是判断View是可点击状态,这个默认就是可点击的,第三个条件执行了我们传进去对象的onTouch方法,并且根据下面的条件判断,我们onTouch的返回值决定着onTouchEvent方法是否得到执行
- 看一下onTouchEvent方法做了哪些事情
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//如果可点击,进入下面的switch
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
if (!focusTaken) {
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_MOVE:
}
return true;
}
return false;
}
- 删掉大多数代码,在up事件的情况下,满足条件时会进入performClickInternal();方法
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
- 可以看到,最终是执行了 li.mOnClickListener.onClick(this);这个方法,这个mOnClickListener也就是我们通过setOnClickListener方法设置进去的对象
- 那么,VIew的事件到这里就结束了
View部分的总结
- 当事件分发到View的时候,会先执行onTouchEvent方法,然后根据onTouchEvent的返回值确认是否执行onClick方法
- 一旦onTouch方法返回true或者onClick方法得到执行,那么dispatchTouchEvent这个方法就会返回true,否则返回false
- 上面的结论可以根据dispatchTouchEvent里面的代码推出来,这里我们需要记住,这个dispatchTouchEvent的返回值对ViewGroup的事件分发起着一定的影响
- 接下来我们来看ViewGroup的事件分发
ViewGroup的点击事件
- ViewGroup与View有什么不同呢?就是ViewGroup不仅仅是一个View,他还是一个可以在它内部放子View的View,所以,当你点击ViewGroup的时候,这个点击事件到底是谁来处理呢?
- 还记得我们说过什么吗?View的点击事件是由他的disPatchTouchEvent方法来处理的
- 而普通的View是做了onTouch和onClick两个方法的处理,那我们接着来看看ViewGroup是怎么处理的
- 接着看ViewGroup的dispatchTouchEvent方法
拦截部分
- 这个源码有点长,我就不一次性贴完了,一点点代码分析,读者可以自行对比源码来分析,在刚开始进入disPatchTouchEvent方法的时候,会来到下面的一段代码
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;
}
- 如果读者有注意下面的代码的话,会发现这里的intercepted 变量对下面的事件分发起着非常大的作用,我们先来分析上面的代码
- 可以看到,当是down事件或者mFirstTouchTarget不为空的时候,会尝试去看看到底需不需要拦截,否则直接设置intercepted为true表示拦截
重点在下面
- 这里可以注意一下,如果不是down事件,并且mFirstTouchTarget为空就直接拦截这个逻辑,为什么呢?如果不是down事件,就证明这个事件序列在之前已经经过这里做过判断,如果mFirstTouchTarget为空就表示他的子View中没有成功处理过该事件序列之前的事件,所以就不再交给子View去处理,直接自己来处理,所以直接设置拦截
- 而如果是down事件,我们需要去判断是否不拦截,这个可以理解,如果mFirstTouchTarget不为空的时候,表示当前事件序列之前的事件已经被子View成功处理过,那成功处理过为啥不直接给子View,而要在这里再判断一下拦截与否呢?
- 其实这里就跟我们在处理滑动事件冲突时的做法有一定关系,关于滑动冲突,这里先不说,等把这部分分析完,我们再去看滑动冲突就很清晰了。
- 我上面说的这些东西建议大家一定要理解,或者多看几遍,很重要的逻辑
重点在上面
- 接着来分析
- 下面一行代码,先看一下ViewGroup的FLAG_DISALLOW_INTERCEPT标志位是否为1,如果为1,那么disallowIntercept 就是true,就直接不拦截,否则进入onInterceptTouchEvent(ev);方法再去判断是否拦截
- 那么这里的FLAG_DISALLOW_INTERCEPT字段究竟是什么意思呢?通过查看源代码,我们发现了这个字段被赋值的地方
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
- 先看看是否已经设置这个字段,如果已经设置就不再设置,根据之前的逻辑,这个方法如果传入true,那么,FLAG_DISALLOW_INTERCEPT字段会被设置为1,然后并递归的向上所有父ViewGroup去设置,为1表示直接ViewGroup不拦截,我们可以看到,这个方法是public类型的,所以这个方法可以用来让子View来调用,表示子View期望父View是否需要拦截事件,这个方法的具体用法会在滑动事件冲突处理时用到。
- 来继续我们之前的拦截部分的代码,可以看到我们来到了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;
}
- 这个方法我们不去关心太多,只需要知道他在一般情况下返回false就行了,也就是一般情况下不会拦截
拦截部分的总结
- 总的来说,这段代码主要有两个部分需要注意,一是if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {这句代码的逻辑,这个我在上面的解释已经很清楚了
- 二是FLAG_DISALLOW_INTERCEPT字段和onInterceptTouchEvent方法的配合
接着看拦截之后的事情
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
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.
final ArrayList 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);
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;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
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;
}
resetCancelNextUpFlag(child);
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();
}
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;
}
}
}
- 可以看到,如果事件不拦截,则是主要在上面这个大if语句里面,并且里面又嵌套了一个if语句,大概说一下里面这个if语句
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
- 这个主要是判断是否是down事件,或者是多点触控的down事件,就是说,当你的第一个手指未抬起之前,又有一个手指按下,他也是当做一个down事件来处理的,这里有点多点触控的概念,关于这个多点触控的东西,大家可以去百度百度,这里不再说明,重点的事件分发与这个关系不大
- 紧接着又有一个if语句
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
- 这个newTouchTarget 变量我们可以大概猜到,这个应该是存处理事件的View的,mChildrenCount就是子View的数量了
- 接着看这个语句里面做了什么事情
- 下面两句,得到该事件的坐标
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.
- 然后从上到下扫描子View,查看有没有能接收该事件的View
final ArrayList 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);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
- 这两句是拿到相应序号的子View,不过这里的逻辑不想分析了,不是重点,继续往下看,我们看到有这样一行代码
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;
}
- 可以看到,这里调用了一句dispatchTransformedTouchEvent方法,这个方法就是用来做具体的子View的分发工作的,在经过一系列的判断之后,发现可以给子View去试着分发,然后就会来到这个方法
- 我们进去这个方法看一下
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
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);
}
transformedEvent.recycle();
return handled;
}
- 这个方法有点长,不过稍加分析,我们就可以看到,有两句代码一定会被调用
handled = child.dispatchTouchEvent(transformedEvent);
或者
handled = super.dispatchTouchEvent(transformedEvent);
- 而这里传进来的child是不空的,所以子View去处理
- 这里我说一下我自己的理解,因为我们的点击事件一定会落在某个View上面嘛,如果这个View为空的话,就没法处理啊,就交给当前的ViewGroup去处理,而ViewGroup自己处理事件的逻辑就会跟单个View处理的逻辑一样咯
- 然后我们再看到,这个事件是直接交给dispatchTouchEvent方法去处理的,这也验证了事件的处理时第一时间交给View的dispatchTouchEvent方法,并且他有一个返回值,我们回去看看这个返回值会对事件分发造成什么影响
- 来到我们刚才的地方,如果 dispatchTransformedTouchEvent方法的返回值是true的话,会进入if语句,并且,根据我们之前对View的分析,返回值为true的话,代表当前View消费此事件了,看看View消费事件后会有什么影响
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
- 对mFirstTouchTarget 这个变量赋值,这个target也刚好就是我们刚才传进去的child,emmm,明白了?这个mFirstTouchTarget 就是成功处理了事件的View
- 然后分发这块已经接近尾声了,因为事件已经被处理了啊,不过,这个事件可是被子View处理的,那么ViewGroup的处理时机呢?我们接着往下看
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
- 从上面我们清楚的知道,如果事件被ViewGroup拦截,或者拦截之后View处理的dispatchTouchEvent方法返回false,那么这个mFirstTouchTarget 变量就不会被赋值,所以,这里就是ViewGroup的处理逻辑了,可以看到也调用了这个方法dispatchTransformedTouchEvent,这个方法在前面已经分析过了,但是这里child直接传了个空,所以直接在里面调用super.dispatchTouchEvent(transformedEvent);,这个不就是View的处理逻辑嘛,不过,此时的View正是当前的ViewGroup啊
- 这里最后在思考一个问题,如果刚开始拦截部分的判断是,mFirstTouchTarget 不为空,且拦截的话,会发生什么事情呢
- 那就会进入最后面的代码块
else {
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;
}
- 稍加分析,便能发现,这下会直接进入dispatchTransformedTouchEvent方法,而这个方法传入的便是mFirstTouchTarget 参数,所以,直接交给他去处理就好啦
- 至于为什么会有上面的代码快呢?想一下,如果不是down事件并且mFirstTouchTarget 就表示已经有View来处理这个事件序列了,当然没有必要再去拦截了
- 好的,那到这里,这个事件分发已经看完了,我先总结一下拦截之后的这部分吧
拦截之后的总结
- 先去遍历子View,然后让合适的子View去处理,如果子View处理成功,那么mFirstTouchTarget 将会被赋值,否则不会赋值,那么就会交给当前的ViewGroup去处理
ViewGroup事件分发的总结
- 先判断是否是down事件或者是否有View来处理这个事件序列来判断是否需要拦截
- 然后如果不拦截的话,就去遍历子View查看能处理该事件的View,如果处理成功,就会对mFirstTouchTarget 赋值,接下来的事件序列都会交到此View身上,
- emmmm ,好像就这么多,具体的细节上面的代码分析中已经很清楚了,这里就不再说了,后面,我们再说说滑动事件处理的题外话
题外话,滑动事件冲突处理
- 滑动事件一般表现在ViewGroup和子View都会对同一事件有处理的能力,而这时就需要人为的根据这个事件的具体类型做出不同的判断了
- 具体表现在是否需要ViewGroup去拦截该事件,我们可以手动调用parent.requestDisallowInterceptTouchEvent来对父View的拦截行为做出约束,我们还可以在自己处理的过程中对返回值根据不同的情况做出不同的返回
- 或者直接ViewGroup中根据事件的类型对是否拦截做出决定
- 这里给大家一个提示就是,即便你的View当前处理了事件,但是如果返回的是false的话,下次的事件还会在父ViewGroup里面再做拦截行为的判断的,所以,大家也可以根据这一点做一些具体的事情
- 这个都需要大家去尝试,因为本文的重点不在这里,所以这里就简单的说一下就行了。