您的位置:首页 > 移动开发 > Android开发

【自定义View系列】04--谈谈事件分发

2016-07-09 22:01 399 查看
引言:这部分会分三个模块来讲,先讲View对Touch的处理,再讲ViewGroup的事件分发,最后讲如何解决滑动冲突。

我习惯通过在源码中添加注释来理解源码,以下是我提取出来几个重要方法,将不重要的部分删掉,并且添加了中文注释。

一、先从View讲起

如果一个View(比如Button)接收到Touch,那么该Touch事件首先会传入到它的dispatchTouchEvent( )方法,所以我们从这里开始学习View对Touch事件的处理。

// 返回值表示Touch事件是否被该View消费
public boolean dispatchTouchEvent(MotionEvent event) {
//result的值决定最后该方法的返回值,也就是决定Touch事件是否被消费
boolean result = false;

/***/

if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
//该if判断中一共包含了4个条件,必须同时满足时才表示Touch事件被消费
//在这四个条件中,我们通常最关心的就是最后一个:TouchListener的onTouch()方法。假如这四个条件中的任意一个不满足,那么result仍为false;则进入下一步调用自身的onTouchEvent()
if (li != null && li.mOnTouchListener != null &&
(mViewFlags&ENABLED_MASK)==ENABLED && li.mOnTouchListener.onTouch(this,event)) {
result = true;
}

//调用View自身的onTouchEvent()处理Touch事件,由onTouchEvent的返回值决定result的值(当然,如果在上一步中,如果已经将result设为true,就不会去判断onTouchEvent()了)
if (!result && onTouchEvent(event)) {
result = true;
}
}

/***/

return result;
}


onTouchEvent()是决定事件是否被消耗的最后一道门,如果返回false,则它的父View的onTouchEvent会被调用,否则不会;

先来看看重要部分的源码:

public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();

//如果一个View是disable的,CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE消耗掉,且不会触发onClick事件回调
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}

if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}

//如果View不是disable的,会继续执行,对CLICK,LONG_CLICK,CONTEXT_CLICKABLE进行处理
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_DOWN:
/***/
break;

case MotionEvent.ACTION_MOVE:
/***/
break;

case MotionEvent.ACTION_CANCEL:
/***/
break;

case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
/***/
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
/***/
if (!focusTaken) {
/***/
if (!post(mPerformClick)) {
//performClick()中会回调onClick,所以我们平时常见的onClick回调都是在ACTION_UP的时候触发的
performClick();
}
}
}
}
/***/
}

//这里表示,只要View是enable的,Touch事件都会被消耗掉
return true;
}

return false;
}


引用一张谷歌的小弟画的流程图:



需要注意的点:

onTouch()与onTouchEvent()以及click三者的区别和联系 :

onTouch()与onTouchEvent()都是处理触摸事件的API

onTouch()属于TouchListener接口中的方法,是View暴露给用户的接口便于处理触摸事件,而onTouchEvent()是Android系统自身对于Touch处理的实现

先调用onTouch()后调用onTouchEvent()。而且只有当onTouch()未消费Touch事件才有可能调用到onTouchEvent()。即onTouch()的优先级比onTouchEvent()的优先级更高。

在onTouchEvent()中处理ACTION_UP时会利用ClickListener执行Click事件。所以Touch的处理是优先于Click的

简单地说三者执行顺序为:onTouch()–>onTouchEvent()–>onClick()

View没有事件的拦截(onInterceptTouchEvent( )),ViewGroup才有,请勿混淆

二、ViewGroup的事件分发

Touch事件会从PhoneWindow开始一直传递到最顶层的ViewGroup,然后调用到最顶层的dispatchTouchEvent()

事件分发体系最重要的几个方法:

dispatchTouchEvent(event)

主要完成事件分发的逻辑,只要事件到达该View,一定会调用这个方法,返回值表示是否消耗当前事件。

onInterceptTouchEvent(Event)

判断是否拦截某个事件,返回值表示是否拦截

onTouchEvent(Event)

用来处理Touch事件,返回值表示是否消耗该事件。

他们的关系可以这样表示:

public boolean dispatchTouchEvent(MotionEvent e) {
if(onInterceptTouchEvent(ev)) {
//如果拦截,就自己处理,调用自己的onTouchEvent,如果onTouchEvent返回true就消费掉,如果返回false就传给上层处理
return onTouchEvent(ev);
} else {
//如果不拦截,就走子view的分发流程
return child.dispatchTouchEvent(ev);
}
}


来看看源码:

public boolean dispatchTouchEvent(MotionEvent ev) {

if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

if (actionMasked == MotionEvent.ACTION_DOWN) {
//如果是DOWN事件,进行初始化和还原操作
cancelAndClearTouchTargets(ev);
resetTouchState();
}

// 判断是否需要拦截事件,根据intercepted的值确定
// mFirstTouchTarget用于多点触控
// mFirstTouchTarget不为空,表示有子View消费了Touch事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果是DOWN或者有子View消费,则根据onInterceptTouchEvent判断是否拦截
// ViewGroup的onInterceptTouchEvent默认返回false,也就是不拦截
// 所以我们往往可以在自定义控件中重写这个方法,来决定什么情况下拦截事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
// 如果disallowIntercept == false,就是不允许拦截,可以在子View设置不允许父View拦截
intercepted = false;
}
} else {
// 执行到这里,说明mFirstTouchTarget为null或者不是DOWN事件,需要ViewGroup自己处理此次Touch事件
// 也就是拦截本次Touch事件
intercepted = true;
}

// 如果Touch事件没有被取消也没有被拦截,那么ViewGroup将类型为ACTION_DOWN的Touch事件分发给子View。
if (!canceled && !intercepted) {

if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

// 根据Touch事件的坐标,找到触摸到了哪个子View
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
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);

newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}

/***/

// 找到之后,调用dispatchTransformedTouchEvent,传入子view
// 子View没有消费Touch事件则该方法的返回值为false,此时mFirstTouchTarget仍为null
// 如果子View消费掉了Touch事件那么该方法的返回值为true,然后执行newTouchTarget = addTouchTarget(child, idBitsToAssign);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
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();
// 如果子View消费掉了Touch事件那么该方法的返回值为true,然后执行
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}

// mFirstTouchTarget为空,表示没有子View消耗Touch事件,需要ViewGroup自己处理
if (mFirstTouchTarget == null) {
// 同样也会调用dispatchTransformedTouchEvent,但是传入Null,标明由父View自己处理这次Touch事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
/***/
}

/***/
}
}


最后会调用dispatchTransformedTouchEvent

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;

/***/

// 如果child == null,表明没有子View消费这次Touch事件
if (child == null) {
// 所以会调用super.dispatchTouchEvent,此时,ViewGroup就化身为了普通的View,它会在自己的onTouch(),onTouchEvent()中处理Touch
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}

// 如果child不为空,表示有子View处理这次Touch事件,直接调用child的dispatchTouchEvent
// 当然该view可能是一个View也可能是一个ViewGroup
handled = child.dispatchTouchEvent(transformedEvent);
}

// Done.
transformedEvent.recycle();
return handled;
}


最后会递归的执行子View的这套流程,或者被ViewGroup自身拦截掉,亲自用onTouchEvent处理这次事件

onInterceptTouchEvent()表示是否拦截此次事件,ViewGroup的默认实现是不拦截,return false;所以我们往往可以在自定义控件中重写这个方法,来决定什么情况下拦截事件

总结下来就是:

Touch事件的传递顺序为 :

Activity–>外层ViewGroup–>内层ViewGroup–>View

如果Touch事件在中间某一层被拦截了,DOWN事件将不会再传递给更底层的View

Touch事件的消费顺序为 :

View–>内层ViewGroup–>外层ViewGroup–>Activity

如果Touch事件在中间某一层被消费了,将不会再通知更上层的View,只有当所有子View都不消费Touch事件,顶层ViewGroup才会自己处理这次Touch事件。

三、滑动冲突处理

引发原因:两个可以滑动的View互相嵌套,且滑动方向相同,则会产生滑动冲突。

有两个比较常见的解决方案:

1、在父View中准确地进行事件分发和拦截

比如重写onInterceptTouchEvent()和onTouchEvent(),对事件进行正确的分配,保证在合适的时候Touch时间可以传递给子View

2、使用Google在support.v4包提供的两个支持嵌套滚动的接口:onNestedScrollChild、onNestedScrollParent。(有一个例子,在我的Github上一个快速开发框架里的下拉刷新SwipeLayout中有用到,贴上地址:https://github.com/miomin/Shareward

四、需要注意的地方

一个事件序列指的是从手指按下的一刻起直到手指放开,通常情况下,一个事件序列只能被一个View拦截或消耗,拦截和消耗通常都是在DOWN事件进行,如果不对DOWN时间进行消费,则不会有机会消耗后续的MOVE事件,如果消耗了DOWN事件,后续的MOVE和UP事件同样由这个View消费。(support.v4包中的NestedScrolling接口可以打破这个原则,允许多个View同时处理同一个事件序列)

ViewGroup的onInterceptTouchEvent默认返回false,也就是默认不会拦截事件,交给下层的View来处理。

View没有onInterceptTouchEvent方法,一旦有事件到达,就会调用onTouchEvent。

可点击的View的onTouchEvent默认返回true,也就是消耗事件。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息