当前位置:首页 >  图片

从小白角度探索Android事件分发机制

发布日期:2019-08-26



今日科技快讯


昨日,小米MIX 3在北京故宫博物院正式发布,该机使用磁动力滑盖设计,前后旗舰双摄,售价3299元起。此外,小米还与故宫博物院推出联名特别版,机身背部嵌有祥瑞神兽獬豸纹样,首次配置10GB超大内存,售价4999元。在全面屏上,雷军一直强调小米是开创者。早在2015年就申请了弹射式设计专利,2018年2月份申请了滑盖式设计。此次小米MIX 3采用了磁力滑盖全面屏,快捷操作“一推即达”。


作者简介


明天就是周六啦,提前祝大家周末愉快!

本篇来自你缺少想象力的投稿,分享了对事件分发机制的解析,希望对大家有所帮助。

你缺少想象力的博客地址:

https://blog.csdn.net/IT_XF


概念


说到事件分发机制,这个知识点主要是在自定义view的时候用到,那么什么是事件分发机制呢。

这里我用大白话概述一下:我们在自定义view,或者在使用某个控件,当给这个view或者控件设置事件的时候,比如有setOnTouchListener、setOnClickListener这些方法的时候,这些方法总有一个执行顺序吧,事件分发机制主要就是了解这些方法执行的先后顺序,或者说执行这些事件的顺序和方法之间的关系,比如点击事件,触摸事件,手指上抬下按等等之类的,主要就是要搞清楚这些事件发生的先后顺序和他们之间的关系。

搞清楚这些东西有什么好处呢,首先,即便假设可能由于这些方法名字太像了,所以你还是没有搞清楚这些方法的执行顺序和相互关系,不过在搞清楚的过程中,至少也搞清楚每个方法,就搞清楚这些方法是干嘛的了吧,知道这些方法是干嘛的又能有什么好处呢,至少不仅仅就只会一个setOnClickListener点击事件了吧,如果你搞清楚了setOnTouchListener方法,也许你就可以实现一个view,手指点击按住后拖动,手指放开后,view又回到原来的位置,这种效果,可不是一个简单的setOnClickListener就能够实现的。


代码追踪


(p.s. 以下代码追踪基于Android 8.0的源码,即API 26)

不知道有哪些方法跟触摸点击有关啊,那就看源码吧!从我们最熟知的setOnClickListener开始,setOnClickListener做了啥,跳进View.class里面,发现这个方法长这样:

反正就是赋值,给这个接口赋值,所以我们来看看mOnClickListener在什么地方使用的,代码一顿追踪,mOnClickListener在这里performClick()被使用了:

先不管这里面具体的一个实现流程是啥,知道mOnClickListener在这里被用到就行了,看这个方法名:performClick翻译过来“执行点击”,嗯~靠谱,继续追踪,于是来到了:

看到这里,我们需要稍微总结一下了。为什么要在这里总结呢,因为不一样了啊,哪里不一样了。首先,前面两个方法setOnClickListener和performClick,概念单一,就一个设置具体实现类接口,还有一个执行点击事件嘛,但是onTouchEvent好像跟以上两种不太一样,因为他有个参数MotionEvent,翻译过来运动事件或者手势事件,这个事件包含了我们很多手势操作,比如手指上抬下按,在屏幕上拖动等等。所以完整的onTouchEvent方法是如下形状:

所以我们可以很清晰的看到,点击事件只是在手指抬起的这个行为后执行的,只是用到了这么多操作行为中的其中一个而已。那么我们是不是就可以得出一个结论,performClick()方法其实只有在手指上抬的时候执行,也就是当手指接触屏幕到离开屏幕的这个过程中,performClick()只执行了一次,而onTouchEvent可能就执行了多次,至少2次吧,即上抬和下按。

接下来我们开始追踪onTouchEvent方法,然后发现了以下源码。

看到此次,有人提问 (问我= =)!为啥这里的源码不写成:

因为有个onTouch()方法,待会要讲,主要是由于view有个setOnTouchListener()方法,先不管这些,我们只看上面那个简单的dispatchTouchEvent()方法,这个方法翻译成中文“分发触摸事件”,好像有点谱了,事件分发机制的源头可能就在此处吧。先不管,继续跟踪,我们来到:

继续跟踪:

妈耶!都跑到私有方法去了,不管,继续跟踪:

不管,反正也不知道这个类是干嘛的,继续跟,跟源码死磕到底!

WTF?都跑道setView设置view那里去了,算了算了,弃坑重练,还盛京棋牌是定位到靠谱的dispatchTouchEvent()方法就终止追踪了吧。


触发顺序


现在我们来总结以下有哪些方法,从上到下的顺一遍

根据源码,我们的猜想是按照这样一个顺序,那我们来验证一下,首先写一个类,并且让这个类实现那些事件方法,所以这个类,基本上就长这样了。

鉴于已经知道触摸行为常见的有3种,按下移动抬起,为了使日志最短,所以我们飞快的点击了屏幕,不给手指在屏幕上的移动的机会,得到了如下日志:

为了看的更加清楚,我们改改源码,打印出具体的行为:

日志:

所以这里面的0和1都是啥意思,还有,这些方法长的太像了,我已经蒙圈了,只认识onClick了。

先看看0和1都是啥意思,看源码呗,不是都说源码是最好的老师吗。

好了,知道了,手指按下就是0,手指上抬就是1。上面的日志就被改成下面这副模样:

然后我们根据方法的名字,将方法名字翻译成中文,上面的日志又被改成以下模样:

大家感受一下这个顺序,给你10秒钟。接下来我们进入下一个环节。


详细分析


dispatchTouchEvent

首先我们先来到最初的dispatchTouchEvent方法中去寻觅过程。

(当前源码API 26;不用看这些源码,我就摆摆场面= =)

某些读者表示,这些源码又多又乱,我在看一篇博客,我要怎么看这些源码,里面的变量是啥意思都不知道,还不能进行变量跟踪,我要怎么看,如果是在IDE里面打开这些源码,兴许我还有几分愿意阅读的兴趣。

以上问题就是我平时看博客的时候脑子里面想到的事情,最讨厌贴上一片源码,然后就开始讲道理了,源码看都看不懂,或者说不想看= =

教大家一个小技巧,看老版本的源码,因为Android源码只会越来越多啊,所以老版本的源码肯定比新版本的少。

目前我这里找到的最老的源码只有API 15 的,所以我们来看看API 15里面的事件分发是怎么写的吧,同一个方法dispatchTouchEvent:

是不是觉得少了很多,不过还是挺多的,那我们怎么看呢,就看这里面出现的关键点,源码少了很多,我们就能快速定位我们的关键点在什么地方了。

首先这个方法里面出现了两个很重要的地方,onTouch和onTouchEvent方法,所以把跟这些代码无关的地方,我们就都给筛掉,所以就变成了以下模样:

好像基本就这样了,也不能再怎么筛了,所以趁着源码才这几行的机会,我们好好来看一下,首先是ListenerInfo类,这个类是干啥的,看名字好像是接口监听信息,瞅瞅源码:

果然基本所有的接口都在这里面,当接口很多的时候,用这种方式统一管理接口,真是个不错的方法,学到了,果然看源码还是有很多好处的嘛。

好的,我们继续来看dispatchTouchEvent方法(复制了一遍,免得往上翻)

publicbooleandispatchTouchEvent(MotionEventevent){
...
ListenerInfoli=mListenerInfo;
if(li!=null&&li.mOnTouchListener!=null&&(mViewFlags&ENABLED_MASK)==ENABLED
&&li.mOnTouchListener.onTouch(this,event)){
returntrue;
}

if(onTouchEvent(event)){
returntrue;
}
...
}

ListenerInfo我们已经知道是怎么回事了,就来看看第一个if,因为第一个if就包含我们其中一个关注点onTouch,这个if条件还挺多的,一共有4个条件,我们一个一个看:

1. li != null

我想,这个一个不用我说了吧,就判断这个接口管理类是否为null

2. li.mOnTouchListener != null

判断这个接口是否为null,我们来看看这个值是在哪里赋值的:

publicvoidsetOnTouchListener(OnTouchListenerl){
getListenerInfo().mOnTouchListener=l;
}

仿佛看到常见方法了,或者关键方法了,这个方法是view的。

3. (mViewFlags & ENABLED_MASK) == ENABLED

说到这里,我要夸夸Google的程序员,确实厉害(Google程序员:还用你夸?)

这个&用的很传神,在API 26 的View.class里面有一群这样的注释:

/**
*MasksformPrivateFlags2,asgeneratedbydumpFlags():
*
*|-------|-------|-------|-------|
*1PFLAG2_DRAG_CAN_ACCEPT
*1PFLAG2_DRAG_HOVERED
*11PFLAG2_LAYOUT_DIRECTION_MASK
*1PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL
*1PFLAG2_LAYOUT_DIRECTION_RESOLVED
*11PFLAG2_LAYOUT_DIRECTION_RESOLVED_MASK
*1PFLAG2_TEXT_DIRECTION_FLAGS[1]
*1PFLAG2_TEXT_DIRECTION_FLAGS[2]
*11PFLAG2_TEXT_DIRECTION_FLAGS[3]
*1PFLAG2_TEXT_DIRECTION_FLAGS[4]
*11PFLAG2_TEXT_DIRECTION_FLAGS[5]
*11PFLAG2_TEXT_DIRECTION_FLAGS[6]
*111PFLAG2_TEXT_DIRECTION_FLAGS[7]
*111PFLAG2_TEXT_DIRECTION_MASK
*1PFLAG2_TEXT_DIRECTION_RESOLVED
*1PFLAG2_TEXT_DI九乐棋牌RECTION_RESOLVED_DEFAULT
*111PFLAG2_TEXT_DIRECTION_RESOLVED_MASK
*1PFLAG2_TEXT_ALIGNMENT_FLAGS[1]
*1PFLAG2_TEXT_ALIGNMENT_FLAGS[2]
*11PFLAG2_TEXT_ALIGNMENT_FLAGS[3]
*1PFLAG2_TEXT_ALIGNMENT_FLAGS[4]
*11PFLAG2_TEXT_ALIGNMENT_FLAGS[5]
*11PFLAG2_TEXT_ALIGNMENT_FLAGS[6]
*111PFLAG2_TEXT_ALIGNMENT_MASK
*1PFLAG2_TEXT_ALIGNMENT_RESOLVED
*1PFLAG2_TEXT_ALIGNMENT_RESOLVED_DEFAULT
*111PFLAG2_TEXT_ALIGNMENT_RESOLVED_MASK
*111PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK
*11PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK
*1PFLAG2_ACCESSIBILITY_FOCUSED
*1PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED
*1PFLAG2_VIEW_QUICK_REJECTED
*1PFLAG2_PADDING_RESOLVED
*1PFLAG2_DRAWABLE_RESOLVED
*1PFLAG2_HAS_TRANSIENT_STATE
*|-------|-------|-------|-------|
*/

与(&),一个符号巧妙的搞定了判断两个值是否等于相同,好了不吹了,偏题了= =

总之这个判断大概就是判断该View是否可用。

4. li.mOnTouchListener.onTouch(this, event))

这里,重点环节,回调了onTouch方法,我们就可用在事件onTouchEvent事件执行之前,先一步窥探有什么事件,甚至拦截接下来的事件。

为什么要先一步呢,因为我们在自定义view的时候,可以很方便的重写onTouchEvent方法,但是如果使用的是系统控件,就不能那么方便的得到这些事件了,如果这时候可以巧妙的使用setOnTouchListener,那么就能先一步得到这些事件了。

第一个if的条件分析完了,为了避免再次你们继续翻上去看那个方法,无形增加后摇时间,所以我重新复制一下:

第一个if我们只看了条件,内容就一个return true,我们来看看第二个if,条件居然直接就是onTouchEvent方法的返回值,内容体也是return true。

那我们就根据这点代码来总结一下吧!

不过还是有不必要的代码,我再简化一下吧:

这样看的是不是就足够清楚了,代码就是这样,剔除了那些非核心代码后,核心代码其实就短短几句。

第一个if,我们可以看到,其实这里的onTouch方法是我们手动实现的,使用setOnTouchListener,就可以在这个设置的接口里面具体实现onTouch的内容了。

然后由于我们还可以把控onTouch的返回值,如果我们将onTouch的返回值设为true,那么第一个if就结束了,dispatchTouchEvent也就直接结束了,那么第二个if就不会执行了,相当于我们可以通过onTouch的返回值,直接拦截view自己实现的onTouchEvent方法。假设有个自定义的DragView,可以想拖哪就拖到哪,如果给这个类setOnTouchListener,那么这个控件的拖动方式就全凭你管了啊,想想都刺激。

所以使用setOnTouchListener可以拦截onTouchEvent方法,默默记住这个知识点。

然后我们接着看第二个if,好像onTouchEvent也可以拦截这个if下面的代码哈,然后其他的就没啥了,反正这个if下面又没有什么触摸事件了,这里这个onTouchEvent的返回值是true是false应该都没啥关系了吧,如果你这样想,那么你就错了。别忘了,onTouchEvent可再也不是我们实现的了,这是系统实现的,那还不赶紧进来看看,里面长啥样。

onTouchEvent

老规矩,把API 15的源码搬上来:

publicbooleanonTouchEvent(MotionEventevent){
finalintviewFlags=mViewFlags;

if((viewFlags&ENABLED_MASK)==DISABLED){
if(event.getAction()==MotionEvent.ACTION_UP&&(mPrivateFlags&PRESSED)!=0){
mPrivateFlags&=~PRESSED;
refreshDrawableState();
}
//Adisabledviewthatisclickablestillconsumesthetouch
//events,itjustdoesn"trespondtothem.
return(((viewFlags&CLICKABLE)==CLICKABLE||
(viewFlags&LONG_CLICKABLE)==LONG_CLICKABLE));
}

if(mTouchDelegate!=null){
if(mTouchDelegate.onTouchEvent(event)){
returntrue;
}
}

if(((viewFlags&CLICKABLE)==CLICKABLE||
(viewFlags&LONG_CLICKABLE)==LONG_CLICKABLE)){
switch(event.getAction()){
caseMotionEvent.ACTION_UP:
booleanprepressed=(mPrivateFlags&PREPRESSED)!=0;
if((mPrivateFlags&PRESSED)!=0||prepressed){
//takefocusifwedon"thaveitalreadyandweshouldin
//touchmode.
booleanfocusTaken=false;
if(isFocusable()&&isFocusableInTouchMode()&&!isFocused()){
focusTaken=requestFocus();
}

if(prepressed){
//Thebuttonisbeingreleasedbeforeweactually
//showeditaspressed.Makeitshowthepressed
//statenow(beforeschedulingtheclick)toensure
//theuserseesit.
mPrivateFlags|=PRESSED;
refreshDrawableState();
}

if(!mHasPerformedLongPress){
//Thisisatap,soremovethelongpresscheck
removeLongPressCallback();

//Onlyperformtakeclickactionsifwewereinthepressedstate
if(!focusTaken){
//UseaRunnableandpostthisratherthancalling
//performClickdirectly.Thisletsothervisualstate
//oftheviewupdatebeforeclic欧博平台kactionsstart.
if(mPerformClick==null){
mPerformClick=newPerformClick();
}
if(!post(mPerformClick)){
performClick();
}
}
}

if(mUnsetPressedState==null){
mUnsetPressedState=newUnsetPressedState();
}

if(prepressed){
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
}elseif(!post(mUnsetPressedState)){
//Ifthepostfailed,unpressrightnow
mUnsetPressedState.run();
}
removeTapCallback();
}
break;

caseMotionEvent.ACTION_DOWN:
mHasPerformedLongPress=false;

if(performButtonActionOnTouchDown(event)){
break;
}

//Walkupthehierarchytodetermineifwe"reinsideascrollingcontainer.
booleanisInScrollingContainer=isInScrollingContainer();

//Forviewsinsideascrollingcontainer,delaythepressedfeedbackfor
//ashortperiodincasethisisascroll.
if(isInScrollingContainer){
mPrivateFlags|=PREPRESSED;
if(mPendingCheckForTap==null){
mPendingCheckForTap=newCheckForTap();
}
postDelayed(mPendingCheckForTap,ViewConfiguration.getTapTimeout());
}else{
//Notinsideascrollingcontainer,soshowthefeedbackrightaway
mPrivateFlags|=PRESSED;
refreshDrawableState();
checkForLongClick(0);
}
break;

caseMotionEvent.ACTION_CANCEL:
mPrivateFlags&=~PRESSED;
refreshDrawableState();
removeTapCallback();
break;

caseMotionEvent.ACTION_MOVE:
finalintx=(int)event.getX();
finalinty=(int)event.getY();

//Belenientaboutmovingoutsideofbuttons
if(!pointInView(x,y,mTouchSlop)){
//Outsidebutton
removeTapCallback();
if((mPrivateFlags&PRESSED)!=0){
//Removeanyfuturelongpress/tapchecks
removeLongPressCallback();

//Needtoswitchfrompressedtonotpressed
mPrivateFlags&=~PRESSED;
refreshDrawableState();
}
}
break;
}
returntrue;
}

returnfalse;
}

好了好了,我知道你们直接跳过来了,知道你们不会看,所以我准备了一份终极简化版:

是不是看着眼睛干净多了,去掉的大概都是一些什么view是否可用,能不能点击之类,各种对象的处理和判断啦,大概就这些东西,总之不影响我们研究核心的内容。

可以看到onTouchEvent的返回值直接就是true,也就可以认为是事件分发的终点了。那么我们来看看这个方法里面做了什么事,首先switch区分了触摸事件的类型,上抬下按什么的,然后我们发现手指上抬的时候,执行了一个performClick,关于onTouchEvent方法,其他就没什么好说的了。既然如此,我们就来大概看看performClick里面是些什么东西了,不过我想你们应该猜到了。

performClick

直接上终极简化版吧,一目了然的感觉真好。

没错,就是回调了setOnClickListener里面设置的接口。

讲到这里,那view的事件分发基本就算讲完了,顺便一提,关于

现在我们也就知道了,如果onTouchEvent返回false,会影响的就是点击事件了,也就是说,如果我们在重写onTouchEvent的时候,如果返回值是false,那么就没有点击事件了,不过你要把点击事件设置到手指刚刚触碰到屏幕的那一刻也行。

view 事件分发总结

中华娱乐

现在我们已经看完了view的整个事件分发的流程源码,重要方法也差不多了解了,那么现在我们来归纳总结一下。

大概总结一下就是:

  • 事件分发最开始是在dispatchTouchEvent这里,这个方法主要是将触摸事件传给onTouch和onTouchEvent。

  • onTouch是一个接口的方法,所以我们可以通过setOnTouchListener来自主实现onTouch里面的内容。

  • 通过控制onTouch方法的返回值,我们可以决定是否拦截系统实现的onTouchEvent方法。

  • onTouchEvent方法里面的触摸行为分为很多种,比如手指下按上抬什么的白金会,当手指上抬的时候,onTouchEvent里面会执行点击操作。

  • 当我们在自定义view的时候,重写onTouchEvent时,如果onTouchEvent的返回值设为false,将不会执行点击操作,不过既然都在重写onTouchEvent了,内部你要怎么实现你的点击事件都可以= =

  • 所以,View的事件分发可以这样说,主要方法:

    顺序就是这样一个顺序,上面的方法可以拦截下面的方法,这里的拦截是指不让下面的方法运行。不过我们主要了解的还是后面3个方法。


    ViewGroup


    说完了view的事件后,我们来谈谈ViewGroup的触摸事件,ViewGroup的触摸事件跟View的触摸事件大体上都差不多,只是有一个地方不一样,举个例子?假设我们现在写了这样两个类:

    两个类,一个是ViewGroup,一个是View,就打印下日志,其他啥也没有了。将这个View放进ViewGroup中,我们运算一下试试。打印结果是:

    有疑问吗?

    View倒是没有啥问题,但是这个ViewGroup就。。。

    为啥ViewGroup没有调用onTouch、onTouchEvent、onClick这三个方法,根据view的事件分发机制,我们可以猜测肯定是ViewGroup里面某个方法把onTouch、onTouchEvent、onClick这三个方法给拦截了,既然ViewGroup的dispatchTouchEvent打印出来了,其他的方法却没有打印出来,肯定是dispatchTouchEvent里面做了什么有拦截性质的操作,让我们在源码较少的API 15里面去寻找答案。

    看源码也不知道是哪里,算了看注释吧,突然发现注释里面有一个注释是Check for interception,拦截检查?听名字靠谱!认真瞧瞧:

    代码还是有多又看不懂,不过这个intercepted变量肯定是关键,然后看到了在哪里赋值后,这个方法在我眼中已经变成如下模样了:

    onInterceptTouchEvent这个方法翻译过来“拦截触摸事件”,还有返回值?哇,跟V白金会iew的那些触摸事件很像啊,这个返回值肯定就是控制拦截的,不管三七二十一,我们先看看这个方法的源码:

    是我眼花了吗,还能有这么简单的源码?就返回一个false,根据我们对View的了解,这里返回false,应该是没有拦截才对啊,等等!我们先做个实验。在自定义的ViewGroup里面,重写onInterceptTouchEvent方法,直接返回true,看看效果,用实践出真理。

    然后运行一下再看看:

    哇,全是ViewGroup的东西,原来onInterceptTouchEvent拦截的是View里面的触摸事件啊!

    所以这里的开关就是这个onInterceptTouchEvent的返回值,如果是false就走View的事件,如果是true,就走ViewGroup的事件。

    既然View跟ViewGroup的事件分发机制都摸清楚了,那么我们就来总结一下吧!


    总结


    View 事件分发

    首先说说View的事件分发机制,虽然前面已经总结过一次了,不过在这里再总结一次。

    dispatchTouchEvent(MotionEvent ev)负责处理MotionEvent这些触摸事件,然后按照顺序,这里有3个方法:

    用动画的方式看怎么样?

    正常情况下,程序就跟着这个顺序执行下去了:

    我们可以使用setOnTouchListener来实现onTouch,然后可以通过控制onTouch的返回值,来决定是否继续执行下面的两个方白金会法,返回值为true,则不继续执行,为false,则继续执行。像这样?

    为false,我就不做图了,跟图2类似。

    这里面三个方法,前面执行的方法有决策权,可以决定是否执行他之后的方法,返回值为true,则不执行之后的方法,为false则执行之后的方法。

    ViewGroup事件分发

    老实说,ViewGroup的事件分发机制跟View基本一样,毕竟ViewGroup继承View嘛。跟事件有关的那几个方法也是一样的,都是:

    不过如果执行了ViewGroup默认执行View的这三个方法,不会执行ViewGroup的这三个方法,如果想要执行ViewGroup的这三个方法,我们必须修改ViewGroup的onInterceptTouchEvent方法的返回值,为true则可以执行ViewGroup的触摸事件,为false则执行View的触摸事件。


    欢迎长按下图->识别图中二维码

    或者扫一扫关注我的公众号