今天来看一下Activity的事件分发过程:
事件分发
-
用户通过屏幕与手机交互的时候,每一次点击,长按,移动等都是一个事件;
-
事件分发机制:某一个事件从屏幕传递各个View,由View来使用这个事件(消费事件)或者忽略这个事件(不消费事件),这整个过程的控制。
事件的分发的对象是谁
-
系统将事件封装成一个MotionEvent对象,事件分发的过程就是MotionEvent分发的过程。
事件的类型
- 按下(ACTION_DOWN)
- 移动(ACTION_MOVE)
- 抬起(ACTION_UP)
- 取消(ACTION_CANCEL)
事件序列
-
从手指按下屏幕开始,到手指离开屏幕所产生的一系列事件
传递层级
Activity不是真正控制视图,真正控制视图的是window,
当我们的手指去触摸Activity的时候,事件会经过这小样的一个传递过程:
事件最初会在Activity之上,然后通过Window传递到decorView上,这里的DecorView就是Window所持有的一个DecorView,而DecorView又是继承ViewGroup,事件在这个ViewGroup中进行分发,然后才会分发放到我们创建的子的viewGroup和view上面。事件就是按照上面描述的层级关系传递的。
- Activity->Window->DecorView->ViewGroup->View
在这样的层级中,主要涉及的对象有三个【Activity,viewgroup,view】
我们开始进入源码:
首先从Activity中的dispatchTouchEvent()方法出发:
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
在Activity中的dispatchTouchEvent()中首先判断事件类型是不是ACTION_DOWN,如果是那就开始调用onUserInteraction方法;
public void onUserInteraction() {}
在键盘事件,触摸事件,轨迹球事件等被分发到Activity都会调用这个方法,默认是空实现,如果你的Activity需要和设备交互的话,可以来实现这个方法。
然后通过getWindow()获取到Activity持有的一个Window对象:
public Window getWindow() {
return mWindow;
}
这就可以直接使用Window中的API来处理事件了,接着就是就调Window的superDispatchTouchEvent()方法,如果上述方法都返回的是false,说明事件没人处理,事件就会重新回到Activity中,调用其onTouchEvent()方法进行处理.
注意:Window是个抽象类,虽然在Window中声明了大量的方法,但是都没有实现,
Window是顶级窗口外观和行为策略抽象基类。此类的实例应用作添加到窗口管理器的顶级视图。它提供标准的ui策略,如背景、标题区域、默认密钥处理等。此抽象类的唯一现有实现是android.view.phonewindow,需要窗口时应该实例化它。
在Activity的内部API中有个attach()方法:
final void attach(Context context, ... Window window, ActivityConfigCallback activityConfigCallback) {
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
}
所以,在attach()方法中,将PhoneWindow实例化,并将对象赋值给了mWindow。在getWind()方法中返回的就是一PhoneWindow对象,事件调用的就是PhoneWindow中的superDispatchTouchEvent方法。
那我们到PhoneWindow中看看superDispatchTouchEvent()方法做了些什么:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
这里是调用了DecorView的superDispatchTouchEvent()方法。mDecor是DecorView的对象,就是说Activity持有Window的对象,Window又持有DecorView的对象.
我们进入DecorView中看看superDispatchTouchEvent()做了什么:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView是调用了super.dispatchTouchEvent(event);它也是向上传递的,我们就看看DecorView的继承情况:DecorView继承与FrameLayout,但是FrameLayout中并没有dispatchTouchEvent,不过FrameLayout又继承与ViewGroup。是的,最后是调用了ViewGroup中的dispatchToucheEvent();
现在,事件分发就走到了ViewGroup中。
ViewGroup 的dispatchTouchEvent()是事件分发的主要的大逻辑,虽然代码量大,但是源码中有大量的注释,帮助我们理解其中的代码逻辑。Google真良心。但是我们分析可以看出它主要做的是哪些事:
- 去判断是否需要 拦截事件
- 在当前ViewGroup中找到用户真正点击的View
- 分发事件到View上。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
// 验证事件是否符合安全策略,
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
...
// 检查是否需要拦截.
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;
}
...
for (int i = childrenCount - 1; i >= 0; i--) {
// 尝试找到用户真正点击的view(先获取view的索引,然后获取真正的view)
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
// 将事件转换为特定子view的坐标空间,事件就进入下一层是分发
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
//将指定子项的触控目标添加到列表的开头,完成对mFirstTouchTarget的赋值并终止对子元素的遍历
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
dispatchTouchEvent主要做的事
在dispatchTouchEvent()方法开始,就需要使用onFilterTouchEventForSecurity方法对事件进行安全策略的检查:
/**
* Filter the touch event to apply security policies.
*
* @param event The motion event to be filtered.
* @return True if the event should be dispatched, false if the event should be dropped.
*
* @see #getFilterTouchesWhenObscured
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
判断的规则是什么呢?首先判断mViewFlags 是不是设置为了FILTER_TOUCHES_WHEN_OBSCURED(当视图窗口被遮盖时,该视图需要过滤触摸事件); 然后事件的标志是不是触摸事件确实被遮挡,上面两个条件都满足,说明窗口被遮盖,触摸事件丢弃,否则返回true,进行正常的事件分发。
然后检查事件是否需要拦截,我们可以看到只有一下两种情况才会对事件进行拦截:
一种是actionMasked == MotionEvent.ACTION_DOWN:说明事件是down事件
第二种是 mFirstTouchTarget != null,这个mFirstTouchTarget是TouchTarget链表中的第一个touch target;mFirstTouchTarget不为空说明有ViewGroup的子View或子ViewGroup中,有可以接受触摸事件的。那么,就将触摸事件分发给这些可以接受触摸事件的子View或子ViewGroup。
此时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.即不会拦截事件,事件还是需要进行分发的.
所以接下来就要寻找子view.
for (int i = childrenCount - 1; i >= 0; i--) {
// 尝试找到用户真正点击的view(先获取view的索引,然后获取真正的view
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
}
通过遍历子view的列表以此获取子view,先通过getAndVerifyPreorderedIndex()方法来获取子view的ID,让后将子view的ID传入getAndVerifyPreorderedView()方法来获取子view.
在获取得到子view之后,通过dispatchTransformedTouchEvent()方法,将事件传递到子view当中,实际上dispatchTransformedTouchEvent()是调用子view的dispatchTouchEvent().
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
...
}
在dispatchTransformedTouchEvent中有这么一段代码,就是说如果当前的child 为空就会调用父元素的dispatchTouchEvent()方法,当前事件如果下面处理不了,就会往上传.
如果子view的dispatchTouchEvent()方法返回true.就是说事件进入了子view.那么此时viewGroup中就会对调用addTouchTarget()方法对mFirstTouchEvent进行赋值,并结束当前的子view遍历,到此事件就进入了子view中.
view中的dispatchTouchEvent
事件进入子view中,首先也是进过子view的dispaTouchEvent()方法,,下面我们近看看View中是怎么处理事件的.
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
...
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)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
在View中,也会对事件进行安全策略的检查,在符合安全策略后,
如果当前事件是鼠标拖动滚动条的事件就返回true;表示消费了这条事件,
然后系统进行了事件监听的操作,如果li != null && li.mOnTouchListener != null,说明当前的view注册了事件监听,并且当前的view的标志为enable状态,而且调用事件监听中的onTouch()方法返回了true.那么当前view就会返回true,就是消费了事件;
如果事件监听中的onTouch()方法反悔了false,就会调用view的onTouchEvent()方法,如果onTouchView()方法中返回了true,就是说onTouch()方法没有处理才会在onTouchEvent()中进行处理.
到此,我们将事件分发的流程大概梳理完了.