Android事件分发机制浅谈(二)--源码分析(ViewGroup篇)

时间:2023-05-29 17:42:14

上节我们大致了解了事件分发机制的内容,大概流程,这一节来分析下事件分发的源代码。

我们先来分析ViewGroup中dispatchTouchEvent()中的源码

public boolean dispatchTouchEvent(MotionEvent ev){
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();
}
... 
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
        intercepted = true;
    }
...
}

可以发现这里的if判断句中存在两个,当事件为ACTION_DOWN,或mFirstTouchTarget!=null时执行语句,我们可以知道ACTION_DOWN表示事件按下,那么mFirstTouchTarget那。我们在下面可以找到它的源码

  private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
} /**
* Cancels and clears all touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
} for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets(); if (syntheticEvent) {
event.recycle();
}
}
}

  结合上面dispatchTouchEvent的源码,简单分析,就是当ACTION_DOWN事件发生时调用了cancelAndClearTouchTargets,这里又调用了clearTouchTargets()方法,完成了对mFirstTouchTarget的赋空值。那么就用疑问了,我们在哪里对它进行赋值那?接着往下,我么可以看到

 newTouchTarget = addTouchTarget(child, idBitsToAssign);

   private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}

  这里就明白了,mFirstTarget感觉就想C语言的指针一样,它指向了处理该点击事件的View的子元素。

好的,那么我们回到dispatchTouchEvent()中,在判断为true时,往下分析,它又有了一个布尔值disallowIntercept,它是用来判断子元素是否调用了requestDisallowInterceptTouchEvent()方法的,这里不过多分析了,这个方法主要是为了让ViewGroup不拦截除了ACTION_DOWN以外的点击事件,我们接着分析,可以发下调用了 onInterceptTouchEvent(ev)这个方法,看源码

 public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}

可以看到它默认返回false,即默认不拦截事件。那么到目前为止,如果看过我的流程图的可以知道上半部分已经讲完了,接下来便是ViewGroup是否拦截事件了。

若是拦截的话,即intercepted为true,接着分析dispatchtouchEvent()中的源码。如果看过我上一节的伪代码的话,那么我们应该开始在下面找onTouchEvent了。好,接着看

 if (!canceled && !intercepted)

这里有一个判断,我们先判定intercepted为true了,所以先暂时跳过这段代码,接着往下看。

   if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
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;
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()这么一个方法,可算是到了重点了,现在让我们进入源码

  private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
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;
} ....
}

咦,它又调用了View的dispatchTouchEvent,看源码

 public boolean dispatchTouchEvent(MotionEvent event) {
.....
boolean result = false; if (onFilterTouchEventForSecurity(event)) {
//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;
}

略过无用的内容,我们首先可以看到一个listenerinfo,这里面放置了各种listener的信息,先进行判断句的分析,li!=null可以理解,

li.mOnTouchListener != null这个在哪里设置那?

public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}

看到这就熟悉了,这不就是我们往常写的xxx.setOnTouchListener()吗?我们接着看

(mViewFlags & ENABLED_MASK) == ENABLED

这个是确认控件是否是ENABLED的,即是否可点击的,当然一般为true。接着看,哇,神奇的发现了一个方法

 li.mOnTouchListener.onTouch(this, event)

竟然调用了onTouch()方法,连着后面一起看

if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
} if (!result && onTouchEvent(event)) {
result = true;
}

现在是明白了为什么onTouch返回true时,result为true,在下面的判断中当!result为false时,不在执行onTouchEvent()方法了,可算是明白了。

那么到现在为止,当ViewGroup拦截事件时的分析已经结束了,那么当他不拦截的时候那?我们回到dispatchTouchEvent方法中,看如果不拦截时会发生什么。


if (!canceled && !intercepted) {

                // If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
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<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(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;
}
}
}

可以看到这里面的代码,先判断了子元素是否为空,若是为空的话,与拦截相同。不为空时。

获取点击位置,遍历子元素集合,找到位于这个位置的子元素。这里面的preorderedList借鉴一篇博文(preorderedList中的顺序是按照addView或者XML布局文件中的顺序来的,后addView添加的子View,会因为Android的UI后刷新机制显示在上层;假如点击的地方有两个子View都包含的点击的坐标,那么后被添加到布局中的那个子view会先响应事件;这样其实也是符合人的思维方式的,因为后被添加的子view会浮在上层,所以我们去点击的时候一般都会希望点击最上层的那个组件先去响应事件。)

接着看代码,

  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;
}

getTouchTarget去查找当前子View是否在mFirstTouchTarget.next这条target链中的某一个targe中,如果在则返回这个target,否则返回null。在这段代码的if判断通过说明找到了接收Touch事件的子View,即newTouchTarget,那么,既然已经找到了,所以执行break跳出for循环。如果没有break则继续向下执行走到115行开始到134行,这里你可以看见一段if判断的代码

if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))

将Touch事件传递给了子元素。刚才我们知道dispatchTransformedTouchEvent方法调用了View的dispaTouchEvent(),这里面有一个递归.在dispatchTouchEvent()中如果子View为ViewGroup并且Touch没有被拦截那么递归调用dispatchTouchEvent(),如果子View为View那么就会调用其onTouchEvent()。dispatchTransformedTouchEvent方法如果返回true则表示子View消费掉该事件,同时进入该if判断。

补充一点:在dispatchTouchEvent方法中,如果child==null那么touch事件交给自己的dispatchTouchEvent()处理,当child != null时会调用该子view(当然该view可能是一个View也可能是一个ViewGroup)的dispatchTouchEvent(event)处理,即child.dispatchTouchEvent(event)。

好了,我们接着往下走。break跳出循环后。

  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;
}

我么可以看到如果for循环未找到子View,且mFirstTouchTarget!=null那么就为把newTouchTarget = mFirstTouchTarget,指向最初的TouchTarget。

到目前为止,ViewGroup内的源码就分析完了,有点长,下一节会分析View内的事件分发源代码,不会这么啰嗦了。

借鉴博客:

http://blog.csdn.net/yanbober/article/details/45912661

http://blog.csdn.net/guolin_blog/article/details/9097463/