舌尖上的安卓(android触控事件机制学习笔记录)
对于一个"我们从来不生产代码,我们只是大自然代码的搬运工"的码农来说。对android的触控机制一直是模棱两可的状态,特别是当要求一些自定义的控件和androide的自带控件(比如ViewPager,ListView,ScrollView)高度嵌套在一起使用时。
花了点时间梳理了下,做个笔记。对于一个触控的事件从用户输入到传递到Actigvity到最外层的Viewgroup在到子View,中间过程还可能穿插多个Viewgroup,android在ViewGroup提供了3个方法来控制流程的分发,拦截,和执行。他们按照执行的顺序分别是dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()。
一.主要方法介绍
打个比喻,所有的事件(可以看做美食),要消化没事,首先先要判断桌前的食物你是否喜欢(dispatchTouchEvent相当选择美食的手)。
dispatchTouchEvent(MotionEvent e):根据入参MotionEvent来决定经过onInterceptTouchEvent和标记变量disallowIntercept(稍候会描述)的值,来判断将事件传递到给自己的onTouchEvent()方法还是传递给子View或者者子ViewGroup来再次进行分发,还是直接自己处理掉(调用自身的onTouchEvent方法)。当返回False的时候,表明这次事件不需要分发,把事件扼杀在摇篮里了。:)
当回回True的时候,表明消化了改事件,进入下一步,可以看流程图(用吃货的话来说,就是我没吃饱,再来一打小笼包也没问题)。
当食物进入嘴巴的时候,对于吃不了辣的人来说。假如吃的食物非常非常的辣的话,在进入胃之前舌头是可以判断美食是否好吃选择吞到自己的肚子里的。onInterceptTouchEvent在这里就相当与人的舌头,他是第二层判断食物是否要下咽的器官。
boolean onInterceptTouchEvent(MotionEvent e):决定是否拦截下事件,以便给当前View的TouchEvent来处理。
当改方法返回True的时候,事件会停止向下分发,而调用当前View的onTouchEvent方法来处理,(对于不爱吃香菜的人来说,一晚拉面中要是不小心参杂了一丝香菜,必然是吐之的)。
当返回为False的时候的时候,事件会继续分发到子View进行消化(碰到美食当然要让他到自己的胃里好好犒劳犒劳自己咯)。
onTouchEvent方法才是真正消化食物的胃,事件真正的处理是在这个地方。
boolean onTouchEvent(MotionEvent e) :真正的消息处理的函数,他的返回值,直接影响到事件下次是否回传给父类View的onTouch方法。
当返回值为True时,表明当前View已经消化了该事件,不需要父View继续再消化了(不调用父类onTouchEvent)(吃了个味道不错的肉包,还没吃饱,可以接着吃。
当返回值为False时,表明当前View不能消化该事件,需要父View再消化消化(调用父类onTouchEvent) (人们碰到食物不好吃的时候,有时候也会引起反酸的么,告诉人体,这可不好吃。 以上的的过程都是从 手—>嘴—>胃的循序渐进的过程。手和嘴都是可以调用onInterceptTouchEvent方法来决定是否传递下去。
不过android的触控流程还有一个重要的方法:
void requestDisallowInterceptTouchEvent(Boolean disallowIntercept):这个方法的入参一个bool变量,用来表示是否需要调用onInterceptTouchEvent来判断是否拦截.
该标记如果为True,就如它的字面意思一样——不允许调用onInterceptTouchEvent(),结果就是,所有的父类方法都不会进行拦截,而把事件传递给子View. 该方法属于ViewGroup ,并且是个递归方法,也就是说一旦调用后,所有父类的disallowIntercept都会设置成True。即当前View的所有父类View,都不会调用自身的onInterceptTouchEvent()进行拦截。
requestDisallowInterceptTouchEvent方法的代码
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } //设置当前View的disallowIntercept if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // 递归调用,设置父类的disallowIntercept if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }
二.总结
下基本的规则是:
1. 如果当前的ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return false,那么后续的move, up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理。如果当前ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的onTouchEvent()处理,注意,目标view将接收不到任何事件。
2. 如果当前ViewGroup的onTouchEvent()返回了false,那么该事件将被传递至其上一层次的view的onTouchEvent()处理。
如果当前ViewGroup 的onTouchEvent()返回了true,那么后续事件将可以继续传递给该view的onTouchEvent()处理。
3。 如果当前view.dispatchTouchEvent()返回true,父view中将不再处理该消息,但前提是该消息没有被父view截取,在整个touch消息处理过程中,若处理函数返回true,我们称之为消费了该touch事件,并且后面的父view将不再处理该消息。
注意一点:在整个touch事件过程中,从action_down到action_up,若父ViewGroup的函数onInterceptTouchEvent一旦返回true,消息将不再派发给子view,细分可为两种情况,若是在action_down时onInterceptTouchEvent返回true,不会派发任何消息给子view,并且后面onInterceptTouchEvent函数将不再会被执行,若是action_down时onInterceptTouchEvent返回false ,而后面touch过程中onInterceptTouchEvent==true,父viewGroup会把action_cancel派发给子view,也之后不再派发消息给子view,并且onInterceptTouchEvent函数后面将不再被执行。
很多情况下,在自定义的ViewGroup中继承了onInterceptTouchEvent简单粗暴的返回true,假如自定义的ViewGroup中包含Button,就会发现Button的Onclick方法执行不了。应为要形成一个必须要先要的道一个ACTION_DOWN的事件,onInterceptTouchEvent把Down事件也截取的话,内部的Button自然就不会回掉Onclick了
三.流程图总结
如果仔细查看ViewGroup的onTouchEvent和InterceptTouchEvent这两个类的话,onInterceptTouchEvent的整个过程是没有调用过onTouchEvent的,反之也是,那么onInterceptTouchEvent是如何拦截事件的流程的呢?
答案在dispatchTouchEvent中,dispatchTouchEvent控制了整个流程是被拦截自己消化,还是传递给子View继续消化,是他调用了onInterceptTouchEvent和onTouchEvent进行判断
来看下整个流程用流程图表示如下:
现在看看最最重要的ViewGroup.DispatchTouchEvent()源码 ,探究下刚才的3条规则代码实现的原理
@Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // Check for interception. final boolean intercepted;//拦截的标记变量 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { //调用自身的 onInterceptTochEvent,判断是否需要拦截 intercepted = onInterceptTochEvent(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; } //假如没拦截 if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { if (newTouchTarget == null && childrenCount != 0) { 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 View[] children = mChildren; final boolean customOrder = isChildrenDrawingOrderEnabled(); //遍历所有的子View,并且调用他们的事件分发方法dispatchTouchEvent() for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = children[childIndex]; if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } newTouchTarget = getTouchTarget(child); //newTouchTarget表示事件传递的View目标,当不为空的时候,直接跳出循环 if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); //递归调用子View分发事件方法, if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { mLastTouchDownTime = ev.getDownTime(); mLastTouchDownIndex = childIndex; mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //设置分发目标newTouchTarget为当前View newTouchTarget = addTouchTarget(child, idBitsToAssign); //标记子View的分发结果,为True的话,下面的代码是不会调用当前View的onTouch方法的,也就是规则1生成的原因 alreadyDispatchedToNewTouchTarget = true; break; } } } } } 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; //如果刚才alreadyDispatchedToNewTouchTarget设为True的话,就不执行下面的dispatchTransformedTouchEvent //alreadyDispatchedToNewTouchTarget是由子View的onTouch返回值决定的, if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //执行自身的Touch事件, 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; } } }
public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } //... if (onTouchEvent(event)) {//View的dispatchTouchEvent十分简单,就是调用下自己onTouch事件,并且返回结果。这个结果直接影响以后的事件是否///继续传递 return true; } } //... return false; }
所以,搞清楚3个主要的方法,并且理顺每个方法的返回值,清楚的知道返回值对其他方法的影响,是搞清楚触控模型的关键。就可以做到,需求再如何变,也可以把app的交互做好。