自由笔记-AndroidView模块之View触摸事件分发机制分析

相关方法介绍:

dispatchTouchEvent方法:事件的分发。返回值一般由里面的onTouchEvent方法或者下架View的dispatchTouchEvent方法决定。

onInterceptTouchEvent:事件的拦截,如果Down事件一旦拦截了,那么之后的move和up事件也会一起拦截。事件不会在向下传递。

onTouchEvent:事件的处理。如果在子View中设定了mOnTouchListener,那么会优先调用mOnTouchListener的方法。如果返回true,

那么不会在调用onTouchEvent。如果返回false,那么会继续走onTouchEvent。如果一个View的down事件没有

消耗,直接返回的false,那么其他事件都不会在传递到这个View上。事件开始向上传递。

 

事件传递源头:Activity的dispatchTouchEvent方法。

 

public boolean dispatchTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();

}

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

 

由源码可以知道,触摸事件由getWindow().superDispatchTouchEvent(ev)传递下去,如果下面的UI控件没有消耗该时间

由activity的onToucHEvent去消耗。getWindow().superDispatchTouchEvent(ev)的具体实现如下:

 

@Override

public boolean superDispatchTouchEvent(MotionEvent event) {

return mDecor.superDispatchTouchEvent(event);

}

 

由代码可知,事件由整个窗口布局的根View DecorView负责进行分发。

public boolean superDispatchTouchEvent(MotionEvent event) {

return super.dispatchTouchEvent(event);

}

 

在 DecorView中,直接调用了super.dispatchTouchEvent(event),而DecorView是一个FrameLayout,即该方法

走入了ViewGroup里面的dispatchTouchEvent。接下来具体分析该方法

 

第一段: // 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();

}

如果事件是DOWN事件,那么初始化一些状态。

 

第二段:

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;

}

 

这里主要分析拦截动作。从代码可以看出如果是DOWN事件则一定会进入到onInterceptTouchEvent方法判断是否拦截

如果down事件被父控件拦截了,那么mFirstTouchTarget必定会是null,所以onInterceptTouchEvent不会在执行,

默认后续的事件都返回intercepted = true;表示拦截。而如果不是DOWN事件,那么DOWN事件必须由子控件响应过(mFirstTouchTarget!=null表示之前的DOWN事件被子空间消耗了

否则mFirstTouchTarget会依然是null)才会继续判断是否拦截。如果子控件没有响应过即子控件没有消耗过DOWN事件,那么

该地方不会在进行判断,直接默认拦截,事件不会在传递到下层子控件

 

由此可以得出三个结论:

A、如果父控件拦截了down事件,那么down事件不会在传递给子控件。所有其他事件都不会在传递下去并交由父控件处理

B、如果父控件没有拦截down事件,并且子控件消耗了down事件,那么后续的move up等事件父控件依然会判断是否拦截,如果拦截那么后续的事件不会在往下传递,如果拦截move事件,那么第一个move事件会变成一个cancel事件传递到子控件,详情参见第四段代码分析

C、如果父控件没有拦截down事件,并且子控件也没有消耗down事件,那么后续的事件不会在传递到子控件当中,将有父控件处理。因为mFirstTouchTarget为null

 

disallowIntercept该标志位表示是否拦截,由子控件去调用父控件的requestDisallowInterceptTouchEvent方法设定,如果设定为true,ViewGroup将无法拦截除了ACTION_DOWN以外的其它事件

1、子view不希望父view拦截事件可以调用mParent.requestDisallowInterceptTouchEvent(true)

2、viewgroup的mGroupFlags在下一个cycle来临的时候,FLAG_DISALLOW_INTERCEPT标志位会被清零

3、requestDisallowInterceptTouchEvent这个函数一般不是自己调用的,而是给子View调用的

4、requestDisallowInterceptTouchEvent是解决滑动冲突的大杀器,目前大部分原生控件都是使用

 

 

第三段:

if (!canceled && !intercepted) {

if (newTouchTarget == null && childrenCount != 0) {

final float x = ev.getX(actionIndex);

final float y = ev.getY(actionIndex);

// 找出可以接受事件的子控件

final View[] children = mChildren;

for (int i = childrenCount - 1; i >= 0; i--) {

if (!canViewReceivePointerEvents(child)

|| !isTransformedTouchPointInView(x, y, child, null)) {

ev.setTargetAccessibilityFocus(false);

continue;

}

newTouchTarget = getTouchTarget(child);

if (newTouchTarget != null) {

newTouchTarget.pointerIdBits |= idBitsToAssign;

break;

}

resetCancelNextUpFlag(child);

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

mLastTouchDownTime = ev.getDownTime();

if (preorderedList != null) {

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;

}

}

}

}

 

第三段代码是核心代码,主要做了以下几个事情,前提条件是父控件没有进行拦截和取消事件:

1、从当前触摸区域找出可以相应事件的子控件;

2、通过getTouchTarget方法找到触摸的目标控件,如果之前DOWN事件触摸过,那么直接跳出循环,由后面的代码进行事件传递。

3、如果getTouchTarget方法返回一个null,则之前没有触摸过,那么则通过dispatchTransformedTouchEvent方法传递事件给对应区域的子控件。

 

dispatchTransformedTouchEvent方法是事件传递的关键方法,在该方法中,如果传入的child为null则表示没有找到对应的子控件消耗。那么会由父控件去消耗

但是在第三段代码中,只有找到了子控件才会调用dispatchTransformedTouchEvent方法,然后会继续调用子控件的child.dispatchTouchEvent(event);进入下一个轮训

 

如果子控件child.dispatchTouchEvent(event)方法返回false,表示子控件没有消耗该事件,dispatchTransformedTouchEvent也会为false,那么newTouchTarget和mFirstTouchTarget都会为null

这也就是为什么第二段代码会由结论C了。

 

 

第四段:

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;

}

}

 

这里代码也很好理解,首先,如果在第三段代码没有找到能消耗事件的子控件,或者父控件进行了拦截没有进入到第三段代码中

那么这里会直接调用handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);

child传入会null,那么里面会直接调用super.dispatchTouchEvent(event);会走到View类中的dispatchTouchEvent方法进行事件传递

表示当前触摸事件由当前父控件进行消耗。

 

而如果在第三段代码中找到了消耗down事件,那么在down事件走到这里之后,会直接返回handled=true。而后续的move和up事件

因为找到了mFirstTouchTarget,那么不会再走进入第三段代码,会调用

if (dispatchTransformedTouchEvent(ev, cancelChild,arget.child, target.pointerIdBits)) {

handled = true;

}

这里注意,如果进行了MOVE这个触摸事件拦截,那么会触发一个cancel的事件动作传递给之前消耗DOWN事件的子控件,并且在后面将mFirstTouchTarget置空

这样后续的move事件就不会在传递给之前的子控件了。所以一旦父控件执行了onInterceptTouchEvent并且返回true,那么后续的事件就不会再往下分发。

此时由于子控件已经消耗了down事件,如果后续的move和up事件在这里没有消耗,dispatchTransformedTouchEvent返回false,那么handled也为false,那么后续的

事件不会在传递给父控件去消耗,而是传递给activity中的onTouchEvent方法去处理。

 

 

view对touch事件的处理

 

1、先看view如何分发事件:dispatchTouchEvent

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;

}

}

如果view设置的enable为true并且也设定了touchEvent的监听器,那么就会先响应touchEvent的监听器。如果enable为false或者是没有设定监听器,那么会响应

view的onTouchEvent的方法

 

1、view的onTouchEvent的方法,如果view设置的enable为false,那么会进入下面代码

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

}

这个时候,如果view的clickable和longClickable是true的话,依旧会返回true,也就是说view即使设置的enable为false,还是能够响应touchEvent事件的

 

如果view设置的enable为true的话,那么进入action的判断状态

switch (action) {

case MotionEvent.ACTION_UP:

if (!post(mPerformClick)) {

performClick();

}

break;

case MotionEvent.ACTION_DOWN:

checkForLongClick(0, x, y);

break;

case MotionEvent.ACTION_MOVE:

break;

}

在ACTION_UP的地方,会响应click事件;在ACTION_DOWM的地方,会根据时间响应longClick事件。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章