Android view的事件分发机制是开发中的一个重点,因此掌握它的真正意义是非常必要的。
一.什么是view的事件分发机制呢?
将点击事件传递到具体某个view处理的整个过程,就叫做事件分发。
二.为什么要有事件分发呢?
因为Android的view是树形结构的,view可能会重叠,当点击的地方多的时候,为了解决点击事件传递给谁的时候,就要用到事件分发了,因此也可以看出,事件分发采用的是责任链的设计模式。
三.事件分发的传递对象是哪些?
主要是Activity->ViewGroup->View的传递过程。
四.事件分发中的三个重要方法?
1.public boolean dispatchTouchEvent(MotionEvent ev)
事件传递方法,把事件依照顺序往下传递。
2.public boolean onInterceptTouchEvent(MotionEvent ev)
事件拦截方法,该方法是在事件分发的 dispatchTouchEvent 方法内部进行调用。是用来判断在触摸事件传递过程中,是否拦截某个 事件。如果方法true表示将时间拦截,交给当前view的onTouchEvent进行处理,如果返回false不拦截,继续往下传递。
3.public boolean onTouchEvent(MotionEvent ev)
事件响应方法,这是用来处理具体事件的,在dispatchTouchEvent中调用。
五.了解了事件分发的一些基本概念后,就开始根据事件传递对象的流程来一步步分析事件的分发过程:
1.Activiy事件分发过程:
首先按下一个页面的按钮后,事件第一个传递的对象就是Activity,
因此先分析事件是怎么到达Activity又是怎么往下传递的?
写个touch事件来看下调用栈:
我们知道Android采用handler的消息机制,消息存在messagequeue消息队列中,同过loope轮询的方式来调用消息,我们从调用栈中的最下面的红框中看到,当我们按下按钮时,nativePollOnce()会收到消息,并将事件发送给InputEventReceiver的dispatchInputEvent()
方法,然后继续往下传,流程是这样的:
nativePollOnce(收到消息)->InputEventReceiver(的dispatchInputEvent)-> ViewRootImpl(view的根节点)的WindowInputEventReceiver
->Activity的dispatchTouchEvent。
到达Activity,接下来分析,事件由activity到达view的过程:
这个过程我们从调用栈中就可以很清楚的看出来,流程是这样的:
Activity(dispatchTouchEvent)-> PhoneWindow(superDispatchTouchEvent) -> DecorView(superDispatchTouchEvent) -> ViewGroup(dispatchTouchEvent)
为什么中间会经过PhoneWindow和DecorView呢?
因为Activity中持有一个Window对象,Window中包含一个PhoneWindow实例,PhoneWindow中又持有一个DecorView对象。
2.ViewGroup事件分发过程:
我们通过一个例子来看下事件传递过程,以下是自定义的一个 ViewGroup:
public class CustomLinearLayout extends LinearLayout {
private final String TAG = "CustomLinearLayout";
public CustomLinearLayout(Context context) {
super(context);
}
public CustomLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG,"onTouchEvent ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG,"dispatchTouchEvent ACTION_UP");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
break;
}
return super.onInterceptTouchEvent(ev);
}
使用这个自定义 ViewGroup:
点击button:
btn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onTouch ACTION_DOWN");
break;
}
return false;
}
});
看下log的打印:
根据log的打印我们知道,当我们点击button时首先调用ViewGroup的调用流程是:
ViewGroup的dispatchTouchEvent->ViewGroup的onInterceptTouchEvent->子view的dispatchTouchEvent
现在我们在ViewGroup中对事件进行拦截,即在onInterceptTouchEvent方法中返回true:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
break;
}
return true;
这时看下log的打印:
调用流程:
ViewGroup的dispatchTouchEvent->ViewGroup的onInterceptTouchEvent->ViewGroup的onTouchEvent
说明拦截事件后,事件就不往子view中传递了,就在ViewGroup的onTouchEvent进行处理。这就很好解释了三个方法的作用:
dispatchTouchEvent:首先调用的方法,对事件进行分发
onInterceptTouchEvent:拦截事件,如果拦截返回true,并执行改VierGroup的onTouchEvent,如果返回false则传给下一个view.
onTouchEvent:对事件的处理
搞懂了流程接下来我们来看下源码,这里主要看部分重要的代码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}...
// Handle an initial 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.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
首先我们看dispatchTouchEvent方法中,view按下时的这两个方法,因为ACTION_DOWN是事件的开始,所以cancelAndClearTouchTargets这个方法是进行初始化,而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;
}
这里判断是否拦截事件,这里intercepted = onInterceptTouchEvent(ev);用来敷值,onInterceptTouchEvent默认返回false,不拦截的,所以我们可以重写onInterceptTouchEvent来对事件进行拦截.
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;
这一大段代码主要就是当ViewGroup不拦截事件时,把事件分发到子view,循环遍历完所有的子view后,如果点击事件都没有被消耗,ViewGroup就会自己处理点击事件,如下代码:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
3.View事件分发过程:
这里我们说的view就是最后一层的子view了,所以并不存在拦截不往下分发的方法,这里我们主要分析onTouch中处理事件的分发过程:
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG,"onClick");
}
});
btn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"onTouch ACTION_UP");
break;
}
return false;
}
});
上面的代码我们看一下log的执行(这里点击按钮的时候有稍微移动下):
我们看到执行流程是:
ACTION_DOWN->ACTION_MOVE>ACTION_UP>onClick
可以看到ACTION_DOWN最开始执行onClick最后执行,如果我们在onTouch中返回true会发现onClick就不执行了。
这里源码不做具体分析,看源码的时候主要发现MotionEvent中有个MotionEvent.ACTION_CANCEL需要注意一下:
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
MotionEvent.ACTION_CANCEL官方分析:
当你的手指(或者其它)移动屏幕的时候会触发这个事件,比如当你的手指在屏幕上拖动一个listView或者一个ScrollView而不是去按上面的按钮时会触发这个事件。
一个解决冲突的重要方法使用说明:
requestDisallowInterceptTouchEvent方法:
requestDisallowInterceptTouchEvent方法用于影响父元素的事件拦截策略,requestDisallowInterceptTouchEvent(true),表示不允许父元素拦截事件,这样事件就会传递给子View。一般这个方法子View用的多,可以用来处理滑动冲突问题。
如:
(1)在子View的dispatchTouchEvent方法中,对于ACTION_DOWN事件,通过调用requestDisallowInterceptTouchEvent(true)默认不允许父布局拦截事件,这样后续事件都交给子View处理。
(2)在子View的dispatchTouchEvent方法中,对于ACTION_MOVE事件,默认是子View处理,在需要父布局处理时,调用requestDisallowInterceptTouchEvent(false)方法来让父布局拦截事件,交给父布局处理。