Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
事件分发之ViewGroup.dispatchTouchEvent(Android5.0
我承认看了好几天代码了但是还是很懵。但是大概了解到的是,把ViewGroup的 dispatchTouchEvent() 搞清楚。再把View自带的dispatchTouchEvent()搞清楚。这两点是相当重要的。因为他们可以把View整个传递机制的特点展现出来。
憋了一天大约看出来个门道了,事件传递的特点是沉下去再浮上来。也就是,view 大致可分为 容器类view, 和 非容器类的view。在硬件上的用户事件接收上来的时候,就进行分发,,这段分发过程暂时不要去考究。但最终一趟下来就分发到咱们Activity当前的顶级view-- DecorView的头上来了。调用了DecorView的dispatchTouchEvent(), 别忘了DecorView可是ViewGroup的孙子, 他好像也跟就没重写这个方法。于是也就等于调用了ViewGroup的这个方法。。打断,,就是这个dispatchTouchView,是广义上的事件传递的开始!!
其实整个事件传递机制,就是靠着 dispatchTouchevent(),onTouchEvent() 和 onInterceptTouchEvent()这三个方法撑起来的。但是但是但是重要的事情要搞清楚,,就是dispatchTouchEvent()这个方法,View写了一个,View的子类ViewGroup又重写了。而且和View里的那个逻辑有大大滴不同。(其实想想也明白,dispatchTouchEvent()名字太直白了,就是分发事件,但是ViewGroup相比于View的不同就是它可以有子View,而View是不可以有子view的。这个本质的不同直接决定了你怎么分发。毕竟ViewGroup的那一堆子view怎么着也得分发下去吧。但是View就简单直接了,只顾着自己消费就得了呗。)对于另一个方法onTouchEvent(),只有View里面实现了,ViewGroup里面没实现。 还有就是 onInterceptTouchEvent()这个是ViewGroup独有的方法,目的就是搞拦截事件用的。。
好,那么看下关系哈:
(1)View里,有两个回调函数 :
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
- 1
- 2
(2)ViewGroup里,有三个回调函数 :
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
补充:
在Android中,事件主要包括点按、长按、拖拽、滑动等,点按又包括单击和双击,另外还包括单指操作和多指操作。所有这些都构成了Android中得事件响应。总的来说,所有的事件都由如下三个部分作为基础:
- 按下(ACTION_DOWN)
- 移动(ACTION_MOVE)
- 抬起(ACTION_UP)
好,,,接着上面的说,我们不是谈到,事件分发,从硬件层传到软件层,再经过一堆逻辑走啊走,就走到DecorView的 diaptchTouchEvent()里面了么,,这个decorView,就是ViewGroup的孙子。最终该是调用的ViewGroup的这个方法。所以呢,先看ViewGroup是怎么分发的。解释全部写在代码里面了。
/** * {@inheritDoc} */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // If the event targets the accessibility focused view and this is it, start // normal event dispatch. Maybe a descendant is what will handle the click. if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } boolean handled = false; //首先来个大判断,范围相当之广 if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 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(); } // Check for interception. final boolean intercepted; //当前的事件是down事件,并且 已经找到了能够接受touch事件的view if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //当其他地方调用了,requestDisallowInterceotTouchEvent()时的时候,这句话的结果将是true。即禁止拦截 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //当可以拦截的时候。 if (!disallowIntercept) { //那就调用onInterceptTouchEvent(),这是android源代码唯一调用这个方法的地方 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. //当当前的时间不是down的时候,或者是mFirstTouchTarget是null的时候。 拦截掉!直接设置为true。 intercepted = true; } // If intercepted, start normal event dispatch. Also if there is already // a view that is handling the gesture, do normal event dispatch. if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } // Check for cancelation. //对cancle事件进行检查 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // Update list of touch targets for pointer down, if needed. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; //这个很有意思,如果不是cancle消息,并且,没有拦截住。 //咱们上面intercepted为false的时候,才能过这条。那么除非是程序改变flag,导致强制不拦截。 //或者是,当前可以拦截,但是调用 onInterceptTouchEvent 返回了false。那这种意思不就是不去拦截呗,相当于没拦截住。 //好吧,那么 down, up 的时候都有可能到这一步。 //那么if里面究竟是干嘛的呢? if (!canceled && !intercepted) { // If the event is targeting accessiiblity focus we give it to the // view that has accessibility focus and if it does not handle it // we clear the flag and dispatch the event to all children as usual. // We are looking up the accessibility focused host to avoid keeping // state since these events are very rare. //找到有焦点的view的顶级父容器 View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; //down 或者 move事件 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // always 0 for down 按下操作总是返回0 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // Clean up earlier touch targets for this pointer id in case they // have become out of sync. removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { //根据touch坐标的位置,寻找子view来接收touch事件 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. final ArrayList<View> preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; //遍历子view判断哪个子view能够接收touch事件 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; } //判断这个子view是不是能接收touch事件,或者,点击的位置是不是在这个子view里面 //不是的话,设置为false,并且continue继续循环 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //那么依照上面的逻辑,这样的走法儿,到这里的话,当前的child肯定是一个可以接收当前touch事件的view了 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { //如果这里,不是null,那么就代表newTouchTarget一定是可以接收touch事件的子View了。 //这样的话,就没必要再遍历了,直接break。 // 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; } //但是如果 newTouchTarget 是null 的话,就会走到这一句。 //那代表的究竟是哪种情况呢?就是用户在屏幕上点击了一下, resetCancelNextUpFlag(child); //dispatchTransformedTouchEvent导致其内部会不断的dispatchTouchEvent (),从而形成一个递归式遍历。、 //当这个方法吐出true的时候,也就代表该事件被消耗掉了。当然当前这个时间有可能是down,有可能是up, //但是绝对不可能是cancle事件 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. //能走到这里的话,说明这个child里面的事件已经被消耗掉了。 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(); //调用addTouchTarget()将child添加到 mFirstTouchTarget的链表头部 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; //那么到了这里,代表事件已经被消耗完了,并且自己还做了个记录。改了一下相关数据 //跳出for循环。因为已经处理完了 break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } //出了for循环了, 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; } } } //由此可见这整个大判断里面,基本就是对 down事件,move事件进行处理。 //1 当当前view是拦截状态(要么不是down事件,再或者回调拦截返回了false)的时候,或者是当前事件是cancle的时候。会跳过前面的一大坨 //逻辑直接走到这一步 //2 当前view是不可拦截状态,那么就得走完了前面的一大坨,走到了这一步 //当 == null 的时候,代表的是 压根没有消费touch事件的子组件,或者touch事件被拦截了 //当 != null 的时候,则代表找到了能够消费touch事件的子组件,那么后续的touch事件便可以传递到子view。 // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. //mFirstTouchTarget为null。 //的原因,没有找到消费touch事件的子组件或者touch事件被拦截掉了。 //稍后会仔细看这个方法。。记得上面有代码也调用到了这个方法,但是不同的是参数之间的 //不同,此处的第三个参数直接给了一个null handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { //mFirstTouchTarget != null //即找到了可以消费的touch事件的子view,并且后续的touch事件可以传到该子view。 // 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) { //如果前面利用ACTION——DOWN事件寻找接收条件的子view同时消耗了该事件。 //那么 handled赋值为true。 handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //如果前面没有分发该事件的话,那就分发,很明白了,要么是cancle,要么是move,要么是down //因为前面有一波判断正好把他们过滤掉了,但是依逻辑来看,就是这类事件,可以走到这里。 //那么就专门递归,分发这类的事件。 //但是要注意欧,,递归下来依然是 被消费了才能走到里面的handles = true。 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; } } //对于up 和 move 事件来说,该处理完的也处理了,那么的话,就更改一下各种状态。 //主要的是还原状态。 // Update list of touch targets for pointer up or cancel, if needed. 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; }
这一段方法大致可以分为几个阶段:
1 来的是down吗,是的话,并且不能拦截,那就分发事件,递归遍历,尽量消费。2 走也走完了,那就看看其他的情况处理,是在没有处理,大不了让当前ViewGroup自行消耗,尽力吧。 3 还有一波up,move事件呢,这个没有拦截之说,也没有那么多的限制, 那么就递归分发。。
好,,对于第二条这点主要体现在一个方法上,这个方法很好的解释了事件传递沉下去又浮上来这种现象。
/** * Transforms a motion event into the coordinate space of a particular child view, * filters out irrelevant pointer ids, and overrides its action if necessary. * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. */ //在dispatchTouchEvent里面总共有散出调用了该方法。这是一个很重要的代码。 //是View完成递归分发的关键。 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); //这里有个很重要的的判断 child == null ? 这里可以看调用它的地方,三处有一处 //赤裸裸的给第三个参数设置成了一个true。那接下来的代码寓意何为呢? //所以只要看这三处调用,就知道child == null 到底代表的是什么意思了 //接下来一个个分析三处调用 //1处, 在处理down事件的时候,遍历子ViewGroup找到自己可接受touch事件子view的时候调用了一次,此时第三个参数就是child //2处, 在没有发现自己有可以消耗的view的时候,或者直接被拦截的时候,调用了一次,但是第三个参数就是 null //3处, 在处理除了down事件的其他事件,比如up啥的,调用了一次,此时传入的第三个参数是child。 //这一系列的逻辑就直接说明了一个onTouch事件的传递特点,就是子view无法消费的情况下,那么我就调用 //一下自己父类的diaspatchTouchEvent()尝试着自己消费一下。实在这样还是消费不了的话,那咱就返回false呗。 //也就是这样,,就形成了一个冒泡式传递的样子,沉下去再浮上来。如 1,2,3,4,3,2,1这个样子。 if (child == null) { // 这一句很明白了,调用viewGroup 父类的 dispatchTouchEvent(),那不就是View // 的这个方法么!! handled = super.dispatchTouchEvent(event); } else { // 此处递归的关键 handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } // Calculate the number of pointers to deliver. final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; // If for some reason we ended up in an inconsistent state where it looks like we // might produce a motion event with no pointers in it, then drop the event. if (newPointerIdBits == 0) { return false; } // If the number of pointers is the same and we don't need to perform any fancy // irreversible transformations, then we can reuse the motion event for this // dispatch as long as we are careful to revert any changes we make. // Otherwise we need to make a copy. final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } // Perform any necessary transformations and dispatch. 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; }
再看看传说中View的dispatchTouchEvent方法的实现,,毕竟这个才是终极调用的。
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event) { // If the event should be handled by accessibility focus first. if (event.isTargetAccessibilityFocus()) { // We don't have focus or no virtual descendant has it, do not handle the event. if (!isAccessibilityFocusedViewOrHost()) { return false; } // We have focus and got the event, then use normal event dispatch. event.setTargetAccessibilityFocus(false); } boolean result = false; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; //如果onTouchListener没有设置的话,这条直接过 //这里面以及以下的代码,起码可以说明对于 单个的非容器类view而言,如果ontouch()把 //事件消耗掉了,是不会走到 onTouchEvent()这个方法里的,那么这个方法里相关的 //onclick()回调当然也就不用走了。 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } //如果ontouch的回调没有消费掉这个事件,那么就执行onTouchEvent()方法 //事实上onTouchEvent()大部分情况下返回为true if (!result && onTouchEvent(event)) { result = true; } } //如果到了现在还是没有消费掉的话 if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } // Clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }
发现View里面写的倒是很简单。就是先执行自己的onTouch 回调,如果无法消耗,那就走自己的onTouchEvent()方法。注意了啊,这个onTouchEvent里面大部分其实会返回true,并且里面有个十分炸天的逻辑,直接导致了咱们平常写的onClickListerner的 onClick()方法的回调!!
但是仔细看这个代码,你会发现,,如果你的onTouch()回调返回了个true的话,会导致咱们的onTouchEvent()方法无法执行。那么咱们平常写的 什么onCkickListerner 那里面的逻辑自然也不会执行了。
关于事件传递,看起来花里胡哨的,但是沉下心来好好看代码,还是勉强能看出网上帖子上写的那种过程的。
下面是不是好奇 decorView 的 dispatchTouchEvent 前面都经历了啥东东?其实很简单。。我贴几个代码就知道了。首先事件其实是传递给Activity了,因为Acitivity 实现了一个专门的事件接收 reciver,并且还把自己传递给了wms系统里面,所以当来了事件,自然它的相关方法就会被回调到了。
/** * Called to process touch screen events. You can override this to * intercept all touch screen events before they are dispatched to the * window. Be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev The touch screen event. * * @return boolean Return true if this event was consumed. */ //activity类中的。。 public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } //首先分发一下,,如果该事件没有得到处理的话 if (getWindow().superDispatchTouchEvent(ev)) { return true; } //那就自己消耗试试 return onTouchEvent(ev); }
public boolean superDispatchTouchEvent(MotionEvent event) {//phoneWindow中的方法 return this.mDecor.superDispatchTouchEvent(event); }
看出来了吧,,decorView。。如果还要追怎么调用到Activity中的,建议大家找源码看看。。反正最重要的流程讲完了。