通过前一篇博客View的事件分发机制,从dispatchTouchEvent说起(一)的介绍相信大家对 Android View 事件的分发机制有了很深的理解。我们知道 Android 中 View 是存在于 Activity。 今天我们继续学习 Activity 到 ViewGroup 的事件分发机制。
当我们手指触摸到屏幕时,最先接收到事件的肯定是
Activity
,首先调用的是Activity
的dispatchTouchEvent(event)
,那么我们下面先来看它的源码:
1、dispatchTouchEvent(event)
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
这里我们看到首先它判断了是不是 DOWN 事件,如果是的话调用了
onUserInteraction()
这个方法我们看到在Activity
源码中是空的,它可以让子类去实现。我们这里不需要多做关注。
下面我们发现这里它继续调用了
getWindow().superDispatchTouchEvent(ev)
,这里我们后面再讲。我们看到如果前面返回时false
的话,后面调用了Activity
的onTouchEvent(ev)
。
那么我们继续看
getWindow().superDispatchTouchEvent(ev)
,这里我们之前文章提到过,getWindow()
返回的是 PhoneWindow 对象,那么我们继续看PhoneWindow
,我们会发现它调用了mDecor.superDispatchTouchEvent(event)
,也就是执行到 DecorView 的superDispatchTouchEvent(event)
。我们继续追踪发现,最终掉的是 ViewGroup 的 diapatchTouchEvent(event)
方法。
那我们这里先总结下
dispatchTouchEvent
:Activity==>PhoneWindow==>DecorView==>ViewGroup 2、onTouchEvent(event)
上面的具体到 ViewGroup 我们后面再看,这里我们还是回到之前的如果返回
false
那么就需要调用Activity.onTouchEvent(ev)
,那么它的源码如下:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
这里只是判断了点击事件的边界条件,如果在边界内就直接返回了
false
,否则finish
当前 Activity。
二、ViewGroup 的分发
我们上面看到
Activity.dispatchTouchEvent(ev)
最终调用到ViewGroup.diapstchTouchEvent(ev)
那么下面我们继续看下它的源码:
1、ViewGroup.dispatchTouchEvent(ev)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//标记事件是否被处理
boolean handled = false;
//判断当前窗口是不是模糊窗口如果是则拦截掉,不是则继续分发
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 初始化DOWN事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 检查是否拦截该事件
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); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
//检查是否取消
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;
//如果不拦截并且不取消则继续执行
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 childrenCount = mChildrenCount;
//newTouchTarget触摸目标链表里面存储有子View
if (newTouchTarget == null && childrenCount != 0) {
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添加到newTouchTarget
newTouchTarget = getTouchTarget(child);
//这里去分发TouchEvent
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
}
return handled;
}
这个方法可以说是非常的多,看着就令人头大。
这里我们主要看核心代码,以下重要的代码我再里面都添加了注释。非核心代码我们这里略过。我们这里注意以下几点:
handled: 这个变量是标记触摸事件是否被处理的,默认是false
代表未被消耗。
onFilterTouchEventForSecurity(ev): 这个方法我们看到是一个非常重要的方法,后面几乎所有的代码都在这个方法返回true
之后才执行。那么这个方法是干啥的呢?它其实是判断当前窗口是否是模糊窗口?如果是的话则返回false
,此时将不会在将事件分发给子 View.
intercepted: 标记当前父布局要不要拦截当前事件,一般都是取onInterceptTouchEvent(ev)
的方法。
onInterceptTouchEvent(ev):判断当前 ViewGroup 是否拦截当前事件,一般返回flase
不拦截。我们可以在子类重写该方法。
cancled: 这个变量就是带判断当前手势是不是被取消。如果cancled
和intercepted
都是false
才会去执行后面的分发。
newTouchTarget:这是一个单向链表,里面存储的是触摸目标的 View 。后面调用getTouchTarget(child)
将子 View 的触摸目标添加到 newTouchTarget 。
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign): 这个方法就是去将触摸事件分发给对应子 View,下面我们就来看下这个方法。
I、ViewGroup.dispatchTransformedTouchEvent
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);
//调用子 View 的dispatchTouchEvent(event)
handled = child.dispatchTouchEvent(event);
return handled;
}
这个方法的代码也不少我们来看看核心代码,首先前面判断当前事件是否是取消事件。如果是的话则执行子 View 的
dispatchTouchEvent
。 后面我们看到最终执行到handled = child.dispatchTouchEvent(event);
来返回handled
。到这里我们就发现最终这里调用的是 View 的 dispatchTouchEvent(event)
。所以这里我们可以用下面的伪代码来总结一下 ViewGroup 的事件分发机制。
2、伪代码总结ViewGroup的事件分发
public boolean dispatchTouchEvent(MotionEvent ev){
boolean concume = false;
//调用onInterceptTouchEvent判断是否拦截
if(onInterceptTouchEvent(ev)){
//如果拦截则调用自己的onTouchEvent(ev)消耗
consume = onTouchEvent(ev);
}else{
//如果不拦截,则将事件分发给子View
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
总体来说就是首先判断 ViewGroup 自己拦截不拦截,如果拦截则调用自己的
onTouchEvent(ev)
,如果不拦截则调用子 View 的dispatchTouchEvent(event)
。
三、总结测试
我们还是写一个简单的项目来测试一下,一个 Activity,一个ViewGroup ,一个View。我们分别重写我们上述提到的方法。
写之前我们先思考下面两个问题:
onTouchEvent
还会执行吗?MainActivity.java
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new CustomLayout(this));
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("TAG_紫雾凌寒_MainActivity","===onTouchEvent===="+event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("TAG_紫雾凌寒_MainActivity","===dispatchTouchEvent===="+ev.getAction());
return super.dispatchTouchEvent(ev);
}
}
CustomLayout.java
class CustomLayout extends LinearLayout {
public CustomLayout(Context context) {
super(context);
addView(new CustomView(context));
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e("TAG_紫雾凌寒_CustomLayout","===onInterceptTouchEvent===="+ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev){
Log.e("TAG_紫雾凌寒_CustomLayout","===dispatchTouchEvent===="+ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event){
Log.e("TAG_紫雾凌寒_CustomLayout","===onTouchEvent===="+event.getAction());
return super.onTouchEvent(event);
}
}
CustomView.java
class CustomView extends View {
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e("TAG_紫雾凌寒_CustomView__","===dispatchTouchEvent===="+event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStrokeWidth(5f);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(300,300,280,paint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("TAG_紫雾凌寒_CustomView__","===onTouchEvent===="+event.getAction());
return super.onTouchEvent(event);
}
public CustomView(Context context) {
super(context);
}
}
1、子View也不处理触摸事件
我们回到之前我们的问题,当子 View 也不处理触摸事件的时候,那么触摸事件该如何传递?
对于这样的问题我们模拟一下,上面的代码就是子View对事件没有消耗,我们点击自定义 View 的区域,我们看到日志如下所示:
我们通过日志看到,当子View不处理时,我们看到事件的传递是会返回到 Activity 执行 Activity 的 onTouchEvent(ev).
同样我们可以看到,当执行完 DOWN 事件后,UP 事件只在 Activity 层,并没有传递至 ViewGroup 和 View。
2、子View处理触摸事件那么我们让子View处理触摸事件,也就是
View.onTouchEvent(ev)
返回true
.我们再来看下日志:这里我们看到当View如果处理触摸事件时,DOWN 和 UP事件的传递是一样的。消耗完之后就不会再返回上一层了。我们看到子 View 处理完事件后,ViewGroup 的 onTouchEvent 没再执行了。 3、父布局拦截事件
当我们让 ViewGroup 拦截触摸事件时我们看看日志是不是跟我们前面分析的一样?
我们看到这里跟我们分析的一样,当 ViewGroup 拦截后,就不会传递到 View 。
我们回顾一下本文我们主要讲的 Activity 的触摸事件的分发机制:手指按下屏幕==>Activity==>PhoneWindow==>DecorView==>ViewGroup==View。下面我们还是通过流程图来总结一下它的传递机制:
欢迎在评论区留下你的观点大家一起交流,一起成长。如果今天的这篇文章对你在工作和生活有所帮助,欢迎转发分享给更多人。
同时欢迎大家扫描左侧二维码关注我的公众号和加入我组建的大前端学习交流群,群里大家一起学习交流 Android、Flutter等知识。从这里出发我们一起讨论,一起交流,一起提升。
群号:872749114
往期推荐
View的事件分发机制,从dispatchTouchEvent说起(一)
Android源码分析——View是如何被添加到屏幕的?
Android热修复——深入剖析AndFix热修复及自己动手实现
深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)
深入理解HashMap原理(二)——手写HashMap
作者:紫雾凌寒