Android程序员事件分发机制学习笔记

通过问题来学习一个东西是很好的方法。学习Android中View的事件体系,我也通过给自己提问题,在解决问题的同时也就知道了其中原理。

首先来几个问题起步:

什么是事件?什么是事件分发机制?
在我们通过屏幕与手机交互的时候,每一次点击、长按、移动等都是一个个事件。按照面向对象的思想,这些一个个事件都被封装成了MotionEvent。

分发机制就是某一个事件从屏幕传递给app视图中的各个View,然后由其中的某个View来使用这一事件或者忽略这一事件,这整个过程的控制就是分发机制了。

要注意的是,事件分发机制中,事件是按一个事件序列的形式分发给View的。这一序列由 ACTION_DOWN 开始,经过一系列 ACTION_MOVE 等事件,最后以 ACTION_UP 事件结束。这一个序列中的所有事件,要么被忽略,要么就只能有一个事件能使用。要是同一个序列,比如从按下到移动这一系列的动作,不同的View都能接受的话,那整个界面就会非常混乱,而且逻辑很复杂。

接下来我提出这三个问题:

某一个事件从屏幕一直传递到View上这一过程的大致流程是怎样的?前面说了事件分发的其实是事件序列。那么同一个序列里那么多事件,是怎样的机制只交给一个View的?我们平时在应用开发时,在外部给View设置的的OnClick OnLongClick 的监听,是在哪里被View处理的?

问题一:事件传递的流程是怎样的?
Android中的View是树状结构,如下图所示:

Android程序员事件分发机制学习笔记
每一个Activity内部都包含一个Window用来管理要显示的视图。而Window是一个抽象类,其具体实现是 PhoneWindow类。DecovrView作为PhoneWindow的一个内部类,实际管理着具体视图的显示。他是FrameLayout的子类,盛放着我们的标题栏和根视图。我们自己写的一些列View和ViewGroup都是由他来管理的。因此事件分发的时候,顶层的这些“大View”们实际上是不会对事件有任何操作的,他们只是把事件不断的向下递交,直到我们可以使用这些事件。

所以,事件自顶向下的传递过程应该是这样的:

Activity(不处理)-> 根View -> 一层一层ViewGroup(如果有的话) -> 子View

如果传递到最后我们的子View们没有处理这一事件怎么办呢?这时候就会原路返回,最终传递给Activity。只有当Activity也没有处理这一事件时,这一事件才会被丢弃。

Activity(不处理则丢弃) <- 根View <- 一层一层ViewGroup(如果有的话) <- 子View

具体在传递事件的时候,是由以下三个方法来控制的:

dispatchTouchEvent : 分发事件onInterceptTouchEvent : 拦截事件onTouchEvent : 消费事件
这三个方法有一个共同点,就是他们具体是否执行了自己的功能(分发、拦截、消费)完全由自己的返回值来确定,返回true就表示自己完成了自己的功能(分发、拦截、消费)。不同之处除了功能外,还有使用的场景。dispatchTouchEvent()和onTouchEvent()这两个方法,无论是Activity ViewGroup 还是View,都会被用到。而onInterceptTouchEvent()方法因为只是为了拦截事件,那么Activity和View一个在最顶层,一个在最底层,也就没必要使用了。因此在View 和 Activity中是没有onInterceptTouchEvent()方法的。

我这里自定义几个ViewGroup和View,分别重写他们的这些方法,在重写的时候打上log。在不添加任何监听(即没有View消费事件)的条件下看一下运行结果:

点击外部ViewGroup:

Android程序员事件分发机制学习笔记
点击子View:

Android程序员事件分发机制学习笔记
可以看到,事件分发首先由ViewGroup的dispatchTouchEvent()方法开始,先调用自己的onInterceptTouchEvent()方法判断是否拦截,返回false表示自己没有拦截,那么接下来直接把事件传给子View。子View调用自己的dispatchTouchEvent()方法进行分发,因为View没有onInterceptTouchEvent()方法,所以不存在拦截操作,因此直接将事件交给
自己的onTouchEvent()方法消费。因为我的子View没有使用这个事件,因此

onTouchEvent()方法直接返回了false表示自己没有消费,那么这个事件此时就算是传到底了。因为自己没有消费,因此自己就没有分发出去,那么子View的

dispatchTouchEvent()方法返回false,把这个事件交还给上一层的ViewGroup。

ViewGroup发现这个事件没有子View消费,那么就自己动手吧!将事件传给自己的onTouchEvent()方法消费。可是ViewGroup也没有消费,那么onTouchEvent()方法只能是再返回false了。同理,ViewGroup自己没有消费事件,因此他的

dispatchTouchEvent()方法也返回了false。这段文字说得可能有点乱,那么就贴一张图来演示一下:(图中红色箭头表示事件自顶向下分发的过程,黄色则表示自底向上返回的过程)

Android程序员事件分发机制学习笔记
接下来,我在子View上添加OnClick监听,再看一下点击子View时的运行结果:

Android程序员事件分发机制学习笔记
乍一看,呀,怎么重复打印了两遍log?其实并不是哪里写错了。前面我说了,事件分发分发的是一个事件序列,我添加了点击事件,那么我就要消费点击事件。而点击事件其实是要分成两个事件的,即ACTION_DOWN + ACTION_UP ,只有这样才算是一次点击事件。因此打印了“两遍”log其实是先打印了ACTION_DOWN的分发流程,再打印了一遍ACTION_UP的分发流程,因此会看到最后一行打印了click事件。即,click事件是在ACTION_UP事件发生后才发生的。

然后看看各个方法的返回值。果然由于我的子View明确表示要消费这个事件序列,因此从ACTION_DOWN开始的所有事件就都交给他消费了。所以子View的onTouchEvent的返回值为true,表示自己需要消费这个事件,然后他的dispatchTouchEvent也返回了true,表示这一事件被自己分发了。既然自己的子View消费了事件,ViewGroup就认为这一事件是被自己分发了,因此他的dispatchTouchEvent也就返回了true。还是来一张图更清楚一点:最后,我在上一步的基础上,给ViewGroup的onInterceptTouchEvent()方法返回值强行改为true,表示事件传到这一层的时候就被拦截了,看一下log:

果然,虽然我要在子View消费事件,但是事件在传到子View之前就被ViewGroup拦截了,那么事件就只会由ViewGroup来消费了,所以ViewGroup就把事件传给了自己的onTouchEvent()来消费。再来一张图:

综上,事件分发的大致流程就是这样。

问题二:如何保证统一序列的事件都交给一个View来处理

先上结论:在传递过程中,只要有一个View主动去消费了第一个事件(ACTION_DOWN),那么ViewGroup会将这个View保存起来,之后同一事件序列的其他事件都直接交给这个View来处理。具体怎么操作,需要看一下源码:

//这是ViewGroup dispatchTouchEvent()的源码:

@Override
 public boolean dispatchTouchEvent(MotionEvent ev) {
 //省略前面一部分无关代码
 //handled是返回的结果,表示是否被分发,默认当然是
 boolean handled = false;
 if (onFilterTouchEventForSecurity(ev)) {
 final int action = ev.getAction();
 final int actionMasked = action & MotionEvent.ACTION_MASK;
 // 判断一下是不是ACTION_DOWN,如果是的话,代表一个新的事件序列来临了
 if (actionMasked == MotionEvent.ACTION_DOWN) {
 //要注意一下这两个方法,在这里会做一下相当于是“清零”的操作
 //在这里包含了诸如mFirstTouchTarget=null这样的初始化操作
 cancelAndClearTouchTargets(ev);
 resetTouchState();
 }
 // intercepted是用来记录是否被拦截的结果
 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 {
 // 没有mFirstTouchTarget,同时事件为非ACTION_DOWN,那么就算要在这里拦截了
 intercepted = true;
 }
 //忽略部分拦截相关的代码
 //这两个对象记一下,后面会碰到
 TouchTarget newTouchTarget = null;
 boolean alreadyDispatchedToNewTouchTarget = false;
 if (!canceled && !intercepted) {
 // 这里就开始对事件类型区分了,如果是ACTION_DOWN,那么就算是一个新的事件序列开始
 if (actionMasked == MotionEvent.ACTION_DOWN
 || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
 // 准备一下,接下来开始遍历自己的子View们
 final int childrenCount = mChildrenCount;
 if (newTouchTarget == null && childrenCount != 0) {
 // 获取到点击的坐标,用来从子View中筛选出点击到的VIEW
 final float x = ev.getX(actionIndex);
 final float y = ev.getY(actionIndex);
 // 按从后向前的顺序开始遍历子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);
 // 其实筛选只是将不合适的View们过滤掉
 //一个一个continue就表示在发现View不合适的时候直接进入下一次循环
 if (childWithAccessibilityFocus != null) {
 if (childWithAccessibilityFocus != child) {
 continue;
 }
 childWithAccessibilityFocus = null;
 i = childrenCount - 1;
 }
 if (!canViewReceivePointerEvents(child)
 || !isTransformedTouchPointInView(x, y, child, null)) {
 ev.setTargetAccessibilityFocus(false);
 continue;
 }
 //终于找到了合适的子View,注意这里将子View封装为一个target
 //要是返回的结果不为空就跳出循环
 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;
 }
 //就算返回结果为空也没关系,在这里继续递归的调用子View的dispatchTransformedTouchEvent()
 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;
 }
 }
 if (preorderedList != null) preorderedList.clear();
 }
 //没有找到要接受事件的View
 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;
 }
 }
 }
 //接下来就是对于非ACTION_DOWN事件的分发了,这里有两种情况
 if (mFirstTouchTarget == null) {
 // 1.压根就没有找到要接受事件的view,或者被拦截了,调用了自身的dispatchTransformedTouchEvent()且穿了一个null的View进去,这样有什么用呢?需要后面分析dispatchTransformedTouchEvent()
 handled = dispatchTransformedTouchEvent(ev, canceled, null,
 TouchTarget.ALL_POINTER_IDS);
 } else {
 //2.有View接受ACTION_DOWN事件,那么这个View也将接受其余的事件
 TouchTarget predecessor = null;
 TouchTarget target = mFirstTouchTarget;
 while (target != null) {
 final TouchTarget next = target.next;
 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
 //alreadyDispatchedToNewTouchTarget这个变量在前面View接受ACTION_DOWN事件时设为了true
 //同时这个mFirstTouchTarget也就是那个View封装好的target
 //那么这个返回值handled就为true
 handled = true;
 } else {
 //对于非ACTION_DOWN事件,依然是递归调用dispatchTransformedTouchEvent
 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;
 }
 }
 // 处理ACTION_UP和ACTION_CANCEL
 if (canceled
 || actionMasked == MotionEvent.ACTION_UP
 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
 resetTouchState();
 } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
 final int actionIndex = ev.getActionIndex();
 final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
 removePointersFromTouchTargets(idBitsToRemove);
 }
 }
 if (!handled && mInputEventConsistencyVerifier != null) {
 mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
 }
 return handled;
 }
接下来看看dispatchTransformedTouchEvent()的源码:

//前面在分析dispatchTouchEvent()的时候发现有多处调用了这个dispatchTransformedTouchEvent(),而且有的地方传来的第三个参数是null
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
 View child, int desiredPointerIdBits) {
 final boolean handled;
 //处理ACTION_CANCEL
 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;
 }
 //忽略部分代码……
 if (newPointerIdBits == oldPointerIdBits) {
 if (child == null || child.hasIdentityMatrix()) {
 if (child == null) {
 //如果传来的参数child为空时,调用自身dispatchTouchEvent()
 handled = super.dispatchTouchEvent(event);
 } else {
 //不为空,那么就调用他的dispatchTouchEvent()
 handled = child.dispatchTouchEvent(event);
 }
 return handled;
 }
 } else {
 //...
 }
 if (child == null) {
 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());
 }
 handled = child.dispatchTouchEvent(transformedEvent);
 }
 // Done.
 transformedEvent.recycle();
 return handled;
 }

上面是对dispatchTouchEvent()和dispatchTransformedTouchEvent()的分析,看起来有点乱,这里梳理一下:

首先明确一点,事件分发是从ViewGroup的dispatchTouchEvent()开始的ViewGroup在遇到一个新的事件序列,即事件ACTION_DOWN时,开始遍历自己的所有子View,找到需要接收到事件的View无论是否找到,都会调用dispatchTransformedTouchEvent()方法,区别在于如果找到了,那么在这个方法中传入的是那个View,否则就是nulldispatchTransformedTouchEvent()方法中第三个参数child为空时,会调用父类的dispatchTouchEvent()方法,否则会调用那个child的dispatchTouchEvent()方法。总而言之,都会去调用View类的dispatchTouchEvent()方法。dispatchTransformedTouchEvent()方法是进行具体的事件分发,除了OnClick()等事件外,onTouchEvent()方法就是在这里调用的只要找到了要接受事件的View,就会将他封装为一个target,保存起来,后续的其他事件都由他来接受
问题三:OnClick OnLongClick等对外的监听是在哪里处理的?

首先想一想一个很简单的逻辑,OnClick事件是先ACTION_DOWN之后再ACTION_UP,所以必定要在onTouchEvent()处理。同理,OnLongClick是在保持ACTION_DOWN一段时间后发生,因此也要在onTouchEvent()中处理。看看源码,发现果然是在这里:

//以下源码均为忽略了不想关部分,只保留了重点

public boolean onTouchEvent(MotionEvent event) {
 //...
 if (((viewFlags & CLICKABLE) == CLICKABLE ||
 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
 (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
 switch (action) {
 case MotionEvent.ACTION_UP:
 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
 // 处理click
 if (!focusTaken) {
 if (mPerformClick == null) {
 mPerformClick = new PerformClick();
 }
 if (!post(mPerformClick)) {
 performClick();
 }
 }
 }
 }
 break;
 case MotionEvent.ACTION_DOWN:
 // a short period in case this is a scroll.
 if (isInScrollingContainer) {
 //...
 } else {
 // 处理longclick
 setPressed(true, x, y);
 checkForLongClick(0, x, y);
 }
 break;
 case MotionEvent.ACTION_CANCEL:
 setPressed(false);
 //...
 mIgnoreNextUpEvent = false;
 break;
 case MotionEvent.ACTION_MOVE:
 //...
 break;
 }
 return true;
 }
 return false;
}
根据前面的分析,在View的dispatchTouchEvent()方法中,会对

public boolean dispatchTouchEvent(MotionEvent event) {
 //...
 boolean result = false;
 if (mInputEventConsistencyVerifier != null) {
 mInputEventConsistencyVerifier.onTouchEvent(event, 0);
 }
 //...
 if (onFilterTouchEventForSecurity(event)) {
 if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
 result = true;
 }
 //只要获取到的ListenerInfo不为空,就说明我们设置了监听,那么就会认为我们想让这个View处理所有事件
 ListenerInfo li = mListenerInfo;
 if (li != null && li.mOnTouchListener != null
 && (mViewFlags & ENABLED_MASK) == ENABLED
 && li.mOnTouchListener.onTouch(this, event)) {//所以会在这里执行onTouch()
 result = true;
 }
 //而如果没有处理,那么再调用onTouchEvent(),直到onTouchEvent()也返回false才会认为该View不消费事件
 if (!result && onTouchEvent(event)) {
 result = true;
 }
 }
 return result;
}

可以看到,在View的dispatchTouchEvent()方法中,会通过查看是否由设置监听器等方法来判断是否要消费事件。onTouchEvent()方法永远会调用,click和longclick都在这里面。而无论内部如何处理,只要返回了true,就会认为消费了这一事件。

分析就到这了,作为一个小菜鸡,分析过程难免有些错误和疏漏,欢迎在评论区讨论~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值