当前位置: 首页 > 科技观察

一篇彻底理解Android事件分发机制的文章

时间:2023-03-18 16:14:49 科技观察

前言在android开发中,经常会遇到滑动冲突(比如ScrollView或者SliddingMenu与ListView的嵌套),需要对android事件有深刻的理解响应机制来解决,事件响应机制已经是android开发者必备的知识了。这也是面试官在面试求职时经常会问到的问题。事件响应相关的常用方法构成了用户在手指与屏幕接触过程中通过MotionEvent对象产生的一系列事件。它有四种状态:MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间(所有事件的开始)MotionEvent.ACTION_MOVE:手指在屏幕上移动MotionEvent.ACTION_UP:手指离开屏幕的瞬间MotionEvent.ACTION_CANCEL:取消手势,一般由程序生成,而不是由用户生成。Android中的onClick、onLongClick、onScroll、onFling等事件都是由很多Touch事件组成的(一个ACTION_DOWN,n个ACTION_MOVE,一个ACTION_UP)。android事件响应机制的实现方式是分发(先由外部View接收,然后传递给其内层的最小View)再处理(从最小View单元(事件源)传递给外层反过来。)。复杂性表现在:可以控制各层事件是否继续传递(分发和拦截协同实现),以及事件的具体消费(事件分发也有消费事件的能力)。android事件处理涉及的三个重要函数事件分发:publicbooleandispatchTouchEvent(MotionEventev)当监听到事件时,首先被Activity捕获,进入事件分发流程。(因为activity没有事件拦截,View和ViewGroup有)事件会被传递给最外层View的dispatchTouchEvent(MotionEventev)方法,由它分发事件。returntrue:表示View在内部消化了所有事件。returnfalse:事件将不再在本层分发,由上层控件的onTouchEvent方法消费(如果本层控件已经是Activity,则事件会被系统消费或处理).如果事件分发返回系统默认的super.dispatchTouchEvent(ev),则将事件分发给本层的事件拦截onInterceptTouchEvent方法进行处理事件拦截:publicbooleanonInterceptTouchEvent(MotionEventev)returntrue:表示事件将被被拦截并将被拦截到的事件由该层控件的onTouchEvent处理;returnfalse:表示不会拦截事件,事件可以成功分发给子View。并由子View的dispatchTouchEvent处理。如果返回super.onInterceptTouchEvent(ev),则默认拦截该事件,传递给当前View的onTouchEvent方法,与r??eturntrue相同。事件响应:publicbooleanonTouchEvent(MotionEventev)在dispatchTouchEvent(事件分发)和onInterceptTouchEvent(事件拦截返回true或super.onInterceptTouchEvent(ev))中返回super.dispatchTouchEvent(ev),然后将事件传递给onTouchEvent方法,该方法响应事件,如果返回true,说明onTouchEvent处理完事件后已经消费了该事件。至此,活动结束;如果returnfasle,表示没有响应事件,那么事件会继续传递给上层View的onTouchEvent方法,直到某个View的onTouchEvent方法返回true,如果最顶层View仍然返回false,那么就认为如果该事件没有被消费,在同一个事件系列中,当前View无法再次接收到该事件,该事件将由Activity的onTouchEvent处理;如果returnsuper.dispatchTouchEvent(ev),则表示没有响应该事件,结果与return相同,为false。从上面的流程可以看出,无论dispatchTouchEvent返回true还是false,都不会再分发事件。只有当它返回super.dispatchTouchEvent(ev)时,才说明它有分发到下层的愿望,但是能否分发成功,则需要通过事件拦截onInterceptTouchEvent来审计。事件是否向上传递取决于onTouchEvent的返回值。View源码分析在Android中,ImageView、textView、Button等继承了View但没有重写dispatchTouchEvent方法,所以View的这个方法就是用来进行事件分发的。查看View重要功能的源码:publicbooleandispatchTouchEvent(MotionEventevent){//返回true,说明View内部消化了所有事件。返回false表示View内部只处理ACTION_DOWN事件,事件继续传递给上级View(ViewGroup)。if(mOnTouchListener!=null&&(mViewFlags&ENABLED_MASK)==ENABLED&&mOnTouchListener.onTouch(this,event)){//这里的onTouch方法就是回调。当我们注册OnTouchListener时重写了onTouch()方法returntrue;}returnonTouchEvent(event);}首先判断三个条件:(1)检查按钮是否设置了OnTouchListener()事件;(2)控件是否Enabled;(控件默认启用)(3)按钮中实现的OnTouchListener监听中的OnTouch()方法是否返回true;如果满足条件,该事件将被消费,不再在onTouchEvent中处理。否则,事件将交给onTouchEvent方法处理。publicbooleanonTouchEvent(MotionEventevent){.../*当前的onTouch组件必须是可点击的,比如Button,ImageButton等,其中CLICKABLE为true,则进入if方法,***返回true。如果是ImageView,TexitView,默认是不可点击的View,这里的CLICKABLE为false,***返回false。当然,也会有特殊情况。如果为这些View设置了onClick监听,这里的CLICKABLE也会为true  */if(((viewFlags&CLICKABLE)==CLICKABLE||(viewFlags&LONG_CLICKABLE)==LONG_CLICKABLE)){switch(event.getAction()){caseMotionEvent.ACTION_UP:...if(!post(mPerformClick)){performClick();//实际上是在我们注册的OnClickListener中回调了新的onClick()方法}...break;caseMotionEvent.ACTION_DOWN:...break;caseMotionEvent.ACTION_CANCEL:...break;caseMotionEvent.ACTION_MOVE:...break;}returntrue;}returnfalse;}publicbooleanperformClick(){...//if(li!=null&&li.mOnClickListener!=null){...li.mOnClickListener.onClick(this);returntrue;}returnfalse;}publicvoidsetOnClickListener(OnClickListenerl){if(!isClickable()){setClickable(true);}getListenerInfo().mOnClickListener=l;}只在重写的onTouch()方法中注册OnTouchListener时返回false—>执行onTouchEvent方法—>导致onClick()回调方法返回true—>onTouchEvent方法不执行—>导致onClick()不执行ViewGroup的回调方法源码分析Android中的LinearLayout等五大布局控件都是继承自ViewGroup,而ViewGroup本身又继承自View,所以ViewGroup的事件处理机制对这些控件有效部分源码:publicbooleandispatchTouchEvent(MotionEventev){finalintaction=ev.getAction();finalfloatxf=ev.getX();finalfloatyf=ev.getY();finalfloatscrolledXFloat=xf+mScrollX;finalfloatscrolledYFloat=yf+mScrollY;final/TempRectframe;/m=这个值默认是false,然后我们可以通过requestDisallowInterceptTouchEvent(booleandisallowIntercept)方法改变disallowIntercept的值。/清除mMotionTarget,并在每次ACTION_DOWN时设置mMotionTarget为null){//***点ev.setAction(MotionEvent.ACTION_DOWN);finalintscrolledXInt=(int)scrolledXFloat;finalintscrolledYInt=(int)scrolledYFloat;finalView[]children=mChildren;finalintcount=mChildrenCount;//遍历它的childViewfor(inti=count-1;i>=0;i--){//第二点finalViewchild=children[i];//如果childView是VISIBLE或者child视图正在执行动作动画,也就是说View只能接收到Touch事件frame);//如果屏幕上的触摸点在子View上.setLocation(xc,yc);child.mPrivateFlags&=~CANCEL_NEXT_UP_EVENT;//调用子View的dispatchTouchEvent()方法if(child.dispatchTouchEvent(ev)){//如果child.dispatchTouchEvent(ev)返回trueindicate//事件Consumed,setmMotionTargettothechildViewmMotionTarget=child;//returntruedirectlyreturntrue;}//事件没有得到处理,尝试下一个视图。//不要重新设置事件的位置,这里不是//必须的。}}}}}//判断是ACTION_UP还是ACTION_CANCELbooleanisUpOrCancel=(action==MotionEvent.ACTION_UP)||(action==MotionEvent.ACTION_CANCEL);if(isUpOrCancel){//如果是ACTION_UP或ACTION_CANCEL,则设置disallowIntercept为默认false//如果我们调用了requestDisallowInterceptTouchEvent()方法将disallowIntercept设置为true//当我们解除指法或取消Touch事件时,resetdisallowIntercept为false//所以上面的disallowIntercept默认为false每次我们ACTION_DOWNGroupFlags&=~FLAG_DISALLOW_INTERCEPT;}//Theeventwas'tanACTION_DOWN,dispatchittoourtargetif//wehaveone.finalViewtarget=mMotionTarget;//mMotionTarget为null意思是没有找到消费Touch事件的View,所以我们需要调用ViewGroup父类的//dispatchTouchEvent()方法,也就是View的dispatchTouchEvent()方法if(target==null){//wedon'thaveatarget,thismeanswe'rehandlingthe//eventasregularview.ev.setLocation(xf,yf);if((mPrivateFlags&CANCEL_NEXT_UP_EVENT)!=0){ev.setAction(MotionEvent.ACTION_CANCEL);mPrivateFlags&=~CANCEL_NEXTdis_UP_EVENT.returnsuper);}//代码这个if中的ACTION_DOWN不会被执行,只有ACTION_MOVE//ACTION_UP会到这里来,如果被ACTION_MOVE或ACTION_UP拦截到的Touch事件,dispatchACTION_CANCEL给target,然后直接返回true//表示消费此Touch事件的处理if(!disallowIntercept&&onInterceptTouchEvent(ev)){finalfloatxc=scrolledXFloat-(float)target.mLeft;finalfloatyc=scrolledYFloat-(float)target.mTop;mPrivateFlags&=~CANCEL_NEXT_UP_EVENT;ev.setAction(MotionEvent.ACTION_CANCEL);ev.setLocation(xc,yc);if(!target.dispatchTouchEvent(ev)){}//clearthetargetmMotionTarget=null;//不要将此事件发送到我们自己的视图,因为我们已经准备好//在拦截时看到;我们只是想给出以下内容//eventtothenormalonTouchEvent().returntrue;}if(isUpOrCancel){mMotionTarget=null;}//finallyoffsettheeventtothetarget'scoordinatesystemand//dispatchtheevent.finalfloatxc=scrolledXFloat-(float)target.mLeft;finalfloatyc=scrolledYFloat)t;(floatev.setLocation(xc,yc);if((target.mPrivateFlags&CANCEL_NEXT_UP_EVENT)!=0){ev.setAction(MotionEvent.ACTION_CANCEL);target.mPrivateFlags&=~CANCEL_NEXT_UP_EVENT;mMotionTarget=null;}//如果ACTION_MOVE,ACTION_DOWN不拦截如果是,直接派发Touch事件给targetreturntarget.dispatchTouchEvent(ev);}1.dispatchTouchEvent的作用:判断事件是否被onInterceptTouchEvent拦截处理当返回super.dispatchTouchEvent时,onInterceptTouchEvent决定事件的流向。当返回false时,事件将继续分发。当它只处理ACTION_DOWN并返回true时,事件将不会继续分发。所有事件(ACTION_DOWN、ACTION_MOVE、ACTION_UP内部处理)2、onInterceptTouchEvent的作用:拦截事件,判断事件是否传递给子View。当此方法返回true时,所有事件都在内部处理。换句话说,后续事件将继续传递给视图的onTouchEvent()处理。当返回false时,事件将向上传递并被onToucEvent接受。如果顶层View中的onTouchEvent也返回false,则该事件就会消失。总结如果ViewGroup找到一个可以处理事件的View,则直接交给子View处理,不会触发自己的onTouchEvent;可以通过重写onInterceptTouchEvent(ev)方法拦截子View的事件(即返回true),如果事件交给自己处理,则执行其对应的onTouchEvent方法。子视图可以调用getParent().requestDisallowInterceptTouchEvent(true);防止ViewGroup拦截其MOVE或UP事件;点击事件产生后,其传递过程如下:Activity->Window->View。***View收到事件后,会按照相应的规则分发事件。如果一个View的onTouchEvent方法返回false,就会交给父容器的onTouchEvent方法处理,一步步处理。如果所有View都没有处理该事件,则交给Activity的onTouchEvent处理。如果某个View开始处理事件,如果不消费ACTION_DOWN事件(即onTouchEvent返回false),同样的事件序列,比如接下来的ACTION_MOVE,是不会交给View处理的。默认情况下,ViewGroup不会拦截任何事件。非容器的视图,如TextView和ImageView,一旦收到事件就会调用onTouchEvent方法。他们自己没有onInterceptTouchEvent方法。一般情况下,它们都是消费事件的(返回true),除非是不可点击的(clickable和longClickable都是false),那么就会被父容器的onTouchEvent处理。点击事件分发流程如下:dispatchTouchEvent——>OnTouchListener的onTouch方法——>onTouchEvent——>OnClickListener的onClick方法。也就是说,我们平时调用的setOnClickListener的优先级最高,所以如果onTouchEvent或者OnTouchListener的onTouch方法返回true,就不会响应onClick方法。