分享

View的事件分发机制

 Dragon_chen 2017-04-21
点击操作发生时,最先调用Activity的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
上面先调用Window的superDispatchTouchEvet方法,实际上是PhoneWindow的superDispatchTouchEvent,如果处理了就返回true则不会调用Activity的onTouchEvent方法来处理触摸事件。而PhoneWindow的superDispatchTouchEvent方法中实际调用类的是DecrView的superDispatchEvent方法。superDispatchEvent方法最终调用的是基类ViewGroup的DispatchEvent方法。在ViewGroup的DispatchEvent方法中代码较多,分段解析
 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();
}
如果是ACTION_DOWN类型的点击事件,代表是一段新的事件序列(ACTION_DOWN,ACTION_MOVE,ACTION_UP),需要重置以前的状态,调用了cancelAndCleaTouchTargets()和resetTouchState();resetTouchState()里有这样一行代码将不拦截的标识位设置为空。   mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;所以ACTION_DOWN事件无法由子View要求不拦截。
// Check for interception.
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
检测是否打断传递,由自己处理触摸事件,如果事件不是ACTION_DOWN类型而是其他事件类型,且mFritstTouchTarget为空(表示没有子VIew处理触摸事件),则打断传递,自己处理触摸事件(intercepted=true)。如果条件不成立,则利用到前面所说的要求不拦截的标识位,看是否设置了(通过子View的requestDisallowInterceptTouchEvent方法),设置了就不拦截intercepted=false,否则就调用onIterceptToucheEvent方法给intercepted赋值。所以onIterceptToucheEvent中返回的boolean值也关系到是否拦截触摸事件。 从优先级来说,较高的是要求不拦截的标志位,其次才是onInterceptToucheEvent。也就是说如果设置了标志位(前提是在ActionDown事件被ViewGroup处理之前设置的),就算覆写的onInterceptToucheEvent返回true也没用。
最开始的判断是:
如果事件不是ACTION_DOWN类型且没有子VIew处理触摸事件(在后面代码可以发现,如果有子View处理触摸事件,mFirstTouchTarget会被赋值),那么intercepted=true.表明这个事件的起始ACTION_DOWN类型事件被当前ViewGroup消费了(如果是被子view消费了,mFistTouchTarget会被赋值),所以其他类型事件也会被拦截,这就是所谓的一旦一个事件序列的ACTION_DOWN被处理了,其他ACTION_UP,ACTION_MOVE都会交给这个ViewGroup处理。这个判断在最前面,所以优先级最高。高于设置要求不拦截的标志位,更高于onInterceptTouchEvent返回true。只要在onInterceptToucheEvent判断ActionDown事件返回true之后,之后再设置要求不拦截的标志位也没用了。不过不太可能出现这种情况。
总而言之结论如下:
1一旦一个事件序列的ACTION_DOWN被ViewGroup处理了,其他ACTION_UP,ACTION_MOVE都会交给这个ViewGroup处理(注意是对ViewGroup而不是View)
2设置要求不拦截的标识位优先级大于onInterceptToucheEvent方法。
如果1和2冲突了,1优先级大于2优先级。
if (!canceled && !intercepted) {
//调用findChildWithAccessibilityFocus方法得到具有焦点的位于最前面的子View
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
findChildWithAccessibilityFocus() : null;
final int childrenCount = mChildrenCount;
//如果事件还没被处理(newTouchTarget为空),且子view个数不等于0
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.
//得到点击事件的x,y坐标值(相对于ViewGroup)后,通过buildTouchDispatchChildList方法建立以view的z值大小排列的view列表

final ArrayList<View> 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);
//遍历ptrotfrtList的view。寻找能处理触摸事件的view
// 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.
//找到前面具有焦点的子view,childWithAccessibiltyFocus==child,具有焦点的子view最优先处理触摸事件。所以直接跳到最后一个元素。结束循环。
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
上面这段代码说明当事件没被当前ViewGroup拦截的时候,优先由具有焦点的子view处理。比如ListView中项布局中的Button常常会抢了触摸事件。这是因为它具有焦点。后面会调用具有焦点的子view的dispatchTouchEvent。
    //调用canViewReceivePointerEvents方法判断,如果view可见或者在执行动画则返回true,表示能处理
//触摸事件,isTransformedTouchPointInView方法判断触摸事件的坐标是否在view内,若不在则跳过
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//调用dispatchTransformedTouchEvent方法,里面调用子view的dispatchToucheEvent方法。可能是View的dispatchTouchEvent,也可能是ViewGroup的dispatchTouchEvent方法
if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
 newTouchTarget = addTouchTarget(child, idBitsToAssign);
上面代码说明如果view不可见且不再执行动画则不能响应事件。如果是view的dispatchTouchEvent方法,在里面调用了view的onTouchEvent方法。如果dispatchTransformedTouchEvent方法返回true则表示事件已经被处理。调用addTouchTarget方法,在方法里面为mFirstTouchTarget赋值。
如果在循环中没有子view可以处理触摸事件或者canceled,intercepted为true(表示被取消,打断)还是会调用dispatchTransformedTouchEvent。不过传的子view参数为空。dispatchTransformedTouchEvent就会调用父view(View类型)的dispatchTouchEvent方法。调用ViewGroup的onTouchEvent方法(实际上是父类View的方法),从而形成事件的回传。如下:
 if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}

再看看如何判断触摸事件的坐标在子View范围中。isTransformTouchPointerView方法
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
先调用transformPointToViewLocal将相对于父ViewGroup的坐标转换成相对于View(child也可能是viewGroup)的坐标。
然后调用view的pointInView方法判断是否在范围中。
public void transformPointToViewLocal(float[] point, View child) {
point[0] += mScrollX - child.mLeft;
point[1] += mScrollY - child.mTop;
public boolean pointInView(float localX, float localY, float slop) {
return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}
可以看出transformPointToVIewLocal方法将父ViewGroup的mScrollx,mScrolly也计算在中。所以当父ViewGroup的通过Scroller移动的时候(Scroller改变mScrollx,mScolly,移动超出ViewGroup本身的部分将会被截断,属性动画改变translationX,)由于进行了计算所以ViewGroup里面的内容的子view会响应触摸事件,而ViewGroup本身也会响应触摸事件(作为上一层的子view响应)。而pointInView方法通过左右边距来计算是否在范围中。
VIew的dispatchTouchEvent方法中先是调用了TouchListener的onTouch方法,如果监听器被设置了,且View是设置了Enable的。onTouch设置result为true就不会调用onTouchEvent方法。可以看出TouchListener.onTouch优先级高于onTouchEvent方法。如果想要两个方法都调用,可以设置View的enable为false。最后返回result。
 if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

if (!result && onTouchEvent(event)) {
result = true;
}
onTouchEvent方法中调用了performClick方法,performClick方法调用了ClickListener的onClick方法
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();

if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}

if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();

// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
从上面代码可知,设置了View的enable为false就不会调用onClick方法。但是消费了触摸事件。
调用onClick方法的前提是view的clickable或者longclickable为true或者contextclickable为true。

最后总结下view事件分发函数的优先级顺序
父viewgroup的dispatchTouchEvent
子view的requestDisallowInterceptTouchEvent
父viewgroup的onInterceptTouchEvent
子view的TouchListener.onTouch
子view的onTouchEvent
子view的ClickListener.onClick
整理下view的事件分发流程:
首先Activity调用DecorView的dispatchTouchEvent分发事件的开始,如果返回false就会调用Activity的onTouchEvent方法由Activity处理触摸事件。
DecorView作为一个ViewGroup(DecorView继承于FrameLayout)
它的dispatchTouchEvent首先判断自己是否处理了action_down事件,是的话就直接打断事件传递。不是根据子view设置的要求不拦截标识位决定是否调用ViewGroup的onInterceptTouchEvent,根据这些判断是否打断事件传递。如果打断则直接调用父类View的dispatchTouchEvent方法由自身来处理触摸事件,返回父类View的dispatchTouchEvent方法的返回值。根据返回值判断是否由上一层(这里DecorView的上一层是Activity)处理触摸事件。这样就能形成事件的回传。

不打断就遍历所有孩子,优先有焦点的,并根据(判断触摸事件是否发生在子view内和子view是否可见是否执行动画)来判断是否由子view可以处理事件,如果有则调用子view的dispatchTouchEvent方法(通过dispatchTransformedTouchEvent方法),如果没有就跳到打断的逻辑处,用父类View的dispatchTouchEvent方法自身处理触摸事件。除此之外,如果子view的dispatchTouchEvent方法也处理不了触摸事件,还是会跳到打断的逻辑处。
子view可以是view类型,也可以是viewgroup类型,viewgroup类型依然走上面的逻辑,view类型则走上面的逻辑中说的打断逻辑处的dispatchTouchEvent方法处理。
最后来比较下view和viewgroup的dispatchTouchEvent的区别:
view分发事件直接进行事件处理,返回是否处理过的bool值
viewgroup分发事件要先进行子view的分发事件处理,子view的分发事件处理完毕,再根据情况(mFirstTouchTarget是否为空)决定是否要自己处理。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多