您的位置:首页 > 其它

View事件的分发机制

2016-05-16 07:49 253 查看
事件分发的3个重要函数 :

dispatchTouchEvent

onInterceptTouchEvent

onTouchEvent

这个三个函数的关系 可以用下述伪代码 描述

public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //表示是否处理事件
if(onInterceptTouchEvent(ev)){ //如果中断了事件
consume = onTouchEvent(ev);  //交给本控件处理
}else{  //没有中断事件  向下分发给它的孩子处理
consume = child.dispatchTouchEvent(ev);
}
return consume;
}


通过上述代码我们可以看得出来三个函数的执行顺序  先执行dispatchTouchEvent 判断是否需要继续分发 如果不需要 那么直接交给自身的OnTouChEvent方法处理即可
其中在View的dispatchTouchEvent函数中  会判断是否设置了mOnTouchListener  如果设置了 会执行它的onTouch方法  所以ontouch事件优先级比 onTouchEvent优先级高


if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null   //判断是否有点击事件 是否enable  点击事件是否处理事件
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

if (!result && onTouchEvent(event)) {
result = true;
}
}


事件传递的一些结论

1.事件序列 —> 一个事件down 开始 到 up 所触发的一系列事件

正常来说 一个时间序列只能被一个View处理 不能将其分离交给多个View

2.某个View一旦决定要拦截一个事件,那么它的 onInterceptTouchEvent方法不再被调用 因为已经拦截了 不需要判断是否需要拦截了

3.ViewGroup 默认不拦截任何事件 源码中默认返回false

4.View没有 onInterceptTouchEvent方法 因为View是最底层的 不存在中不中断的问题 它也没有子布局 所以必须处理事件 直接调用 onTouchEvent 方法

5.View的 onTouchEvent 方法默认都会消耗事件 除非这个view是不可点击的 clickable=false

事件分发的源码解析

首先我们需要知道 当一个点击事件发生之后 首先在Activity中 它的传递过程遵循如下流程 :

Activity –> Window —-> View 之后View再按其 事件分发机制 继续传递

既然是Activity先接受的事件 那我们先看它的 dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();  //此方法未实现  可以实现此方法 来知晓activity何时分发的事件
}
if (getWindow().superDispatchTouchEvent(ev)) {  //此句是关键  将 事件传递给了Window 如果window处理了事件 那么返回true 即可
return true;
}
return onTouchEvent(ev);  //如果window 及其之下都没处理 那么交给自己 Activity 处理
}


然后在 getWindow().superDispatchTouchEvent(ev)方法中 调用了 window的 事件分发函数
在这个函数里 调用了并返回了mDecor.superDispatchTouchEvent(ev)函数  mDecor 是一个DecorView  而这个 mDecor 其实就是我们设置的setContentView的顶级布局
所以这样 就将事件传递到了View中  接下来便是View的事件处理机制了

对于View的事件传递机制 上面第一点中已经说了一部分 我再简要说明一下:
首先调用 dispatchTouchEvent分发事件  由于是View 所以不需要判断需不需要中断
然后就获取ListenerInfo  这个类中存放了 所有的事件 包括 onclickListener  onTouchListener 等   先判断是否有onTouchListener 以及是否enable
如果有 onTouchListener 那么就调用它的 ontouch方法处理事件  在这里可以看出来 ontouch的优先级比 onTouchEvent高   而在 onTouchEvent事件中会有对
onclickListener处理   如果设置了 onclickListener  那么会调用 onclick方法

ViewGroup的事件分发比View的更复杂一些,因为它需要向它的子布局传递 分发事件  我们来分析一下:
首先在 dispatchTouchEvent 方法中


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

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {  //如果是ACTION_DOWN事件  重置touch状态
// 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();
}

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


mFirstTouchTarget 代表的是第一个触摸的目标 也就是 如果有控件处理了事件 这个就是有值的  比如 ViewGroup 的孩子 如果处理了事件
mFirstTouchTarget就不会是空 也就是ViewGroup不能中断事件处理  因为它的孩子处理了  所以通过判断 mFirstTouchTarget!=null可以 设置是否中断


if (!canceled && !intercepted)  这个判断条件如果成立会把事件分发给孩子  也就是如果不中断 就会分发给孩子
下面的代码是分发给子布局的过程
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);
}
上面的代码比较长 但是分发的代码 在这个函数中 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;

// Canceling motions is a special case.  We don't need to perform any transformations
// or filtering.  The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {  //根据孩子是否是null  进行分发
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}


这样就将事件分发到了孩子的手里  如果孩子的某个函数处理了event  并且返回了true  说明 然后就不交给它的父布局处理了  父布局之上都返回了true 不执行他们的touchevent事件


!!!!下面我总结一下 ViewGroup的事件分发机制(一上午的理解。。。)



假设有 这样 的布局结构 RootView(根布局) 下面有 view1 view2 2个孩子 view1也有孩子 view11 和 view12 view2没有孩子

首先事件经过 activity window decorView 传递到 RootView 进行事件分发

因为RootView是个ViewGroup 所以 执行ViewGroup的 dispatchTouchEvent方法 先判断它的孩子是否已经处理过事件 如果处理过 直接中断(通过 mFirstTouchTarget 来进行)

如果没中断 那么遍历它的孩子 也就是 view1 和 view2 这两个孩子 执行 view1 和 view2的 dispatchTouchEvent函数

view1 也是一个ViewGroup 所以 继续执行 它的 dispatchTouchEvent函数 分发给view1的孩子 (注意这里是按照孩子的顺序遍历的 向线性布局这种一般不会覆盖控件的

没什么,但是相对布局和帧布局的时候 上面的控件会有优先处理事件的特权 所以就会产生当控件重叠 只会响应同一级后来添加的控件的事件) 由于view1的孩子 view11是view

所以调用view的 dispatchTouchEvent函数 判断触摸的位置是否在该子控件的范围内或者有没有动画执行 如果在这个范围内继续判断 否则直接返回fasle 如果有ontouchListener 会执行它的 ontouch函数 并返回值 如果没有 执行 onTouchEvent 这时候如果view11处理了

event事件 并且返回了true 在父布局view1的遍历孩子的函数中 就会根据孩子view11的 ontouchEvent函数的返回值 true break出循环 不再遍历下面的孩子

如果没有处理事件 返回了false 父布局会继续遍历子布局 view12 如果view12 返回true 就 跳出循环 父布局view1的返回值为true 如果view1的孩子都没有处理

那么view1的 dispatchTouchEvent 就返回false 让view1的 父布局 RootView 继续遍历孩子 view2 直到返回true 如果依然没有返回true 那么会交给window和 activity 如果一直没返回true

那么最终会由Activity处理

经过上述的分析  我终于明白了 为什么  事件的分发顺序是 从 父布局到子布局 而处理的优先级 是从子布局到父布局  子布局不处理才交给父布局 的原因了
因为父布局会一步一步把事件发给子布局(父布局可以中断) 最后交给适合的子布局  只要有子布局返回了true 那么 父布局就会中断遍历  并返回true 然后父布局的父布局也停止遍历 返回true 一直返回到根布局 事件就处理完毕了
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: