史上最全面解析Android事件分發(fā)機(jī)制

前言

Android事件分發(fā)機(jī)制是Android開發(fā)者必須了解的基礎(chǔ)

網(wǎng)上有大量關(guān)于Android事件分發(fā)機(jī)制的文章邻悬,但存在一些問題:內(nèi)容不全撞蚕、思路不清晰验庙、無源碼分析顶吮、簡(jiǎn)單問題復(fù)雜化等等

今天,我將全面總結(jié)Android的事件分發(fā)機(jī)制粪薛,我能保證這是市面上的最全面悴了、最清晰、最易懂的

本文秉著“結(jié)論先行违寿、詳細(xì)分析在后”的原則湃交,即先讓大家感性認(rèn)識(shí),再通過理性分析從而理解問題陨界;

所以巡揍,請(qǐng)各位讀者先記住結(jié)論痛阻,再往下繼續(xù)看分析菌瘪;

文章較長(zhǎng),閱讀需要較長(zhǎng)時(shí)間阱当,建議收藏等充足時(shí)間再進(jìn)行閱讀

目錄

1. 基礎(chǔ)認(rèn)知

1.1 事件分發(fā)的對(duì)象是誰俏扩?

答:點(diǎn)擊事件(Touch事件)

定義

當(dāng)用戶觸摸屏幕時(shí)(View或ViewGroup派生的控件),將產(chǎn)生點(diǎn)擊事件(Touch事件)

Touch事件的相關(guān)細(xì)節(jié)(發(fā)生觸摸的位置弊添、時(shí)間等)被封裝成MotionEvent對(duì)象

事件類型(4種)

事件類型具體動(dòng)作

MotionEvent.ACTION_DOWN按下View(所有事件的開始)

MotionEvent.ACTION_UP抬起View(與DOWN對(duì)應(yīng))

MotionEvent.ACTION_MOVE滑動(dòng)View

MotionEvent.ACTION_CANCEL結(jié)束事件(非人為原因)

特別說明:事件列

從手指接觸屏幕 至 手指離開屏幕录淡,這個(gè)過程產(chǎn)生的一系列事件

注:一般情況下,事件列都是以DOWN事件開始油坝、UP事件結(jié)束嫉戚,中間有無數(shù)的MOVE事件,如下圖:

即當(dāng)一個(gè)點(diǎn)擊事件(MotionEvent)產(chǎn)生后澈圈,系統(tǒng)需把這個(gè)事件傳遞給一個(gè)具體的View去處理彬檀。

1.2 事件分發(fā)的本質(zhì)

答:將點(diǎn)擊事件(MotionEvent)傳遞到某個(gè)具體的View& 處理的整個(gè)過程

即 事件傳遞的過程 = 分發(fā)過程。

1.3 事件在哪些對(duì)象之間進(jìn)行傳遞瞬女?

答:Activity窍帝、ViewGroup、View

Android的UI界面由Activity诽偷、ViewGroup坤学、View及其派生類組成

1.4 事件分發(fā)的順序

即 事件傳遞的順序:Activity->ViewGroup->View

即:1個(gè)點(diǎn)擊事件發(fā)生后,事件先傳到Activity报慕、再傳到ViewGroup深浮、最終再傳到View

1.5 事件分發(fā)過程由哪些方法協(xié)作完成?

答:dispatchTouchEvent() 眠冈、onInterceptTouchEvent()和onTouchEvent()

下文會(huì)對(duì)這3個(gè)方法進(jìn)行詳細(xì)介紹

1.6 總結(jié)

至此飞苇,相信大家已經(jīng)對(duì)Android的事件分發(fā)有了感性的認(rèn)知

下面,我將詳細(xì)介紹Android事件分發(fā)機(jī)制

2. 事件分發(fā)機(jī)制 源碼分析

請(qǐng)謹(jǐn)記:Android事件分發(fā)流程 =Activity -> ViewGroup -> View

即:1個(gè)點(diǎn)擊事件發(fā)生后,事件先傳到Activity玄柠、再傳到ViewGroup突梦、最終再傳到View

從上可知,要想充分理解Android分發(fā)機(jī)制羽利,本質(zhì)上是要理解:

Activity對(duì)點(diǎn)擊事件的分發(fā)機(jī)制

ViewGroup對(duì)點(diǎn)擊事件的分發(fā)機(jī)制

View對(duì)點(diǎn)擊事件的分發(fā)機(jī)制

下面宫患,我將通過源碼,全面解析事件分發(fā)機(jī)制

即按順序講解:Activity事件分發(fā)機(jī)制这弧、ViewGroup事件分發(fā)機(jī)制娃闲、View事件分發(fā)機(jī)制

2.1 Activity的事件分發(fā)機(jī)制

當(dāng)一個(gè)點(diǎn)擊事件發(fā)生時(shí),事件最先傳到Activity的dispatchTouchEvent()進(jìn)行事件分發(fā)

2.1.1 源碼分析

/**

? * 源碼分析:Activity.dispatchTouchEvent()

? */publicbooleandispatchTouchEvent(MotionEventev){// 一般事件列開始都是DOWN事件 = 按下事件匾浪,故此處基本是trueif(ev.getAction()==MotionEvent.ACTION_DOWN){onUserInteraction();// ->>分析1}// ->>分析2if(getWindow().superDispatchTouchEvent(ev)){returntrue;// 若getWindow().superDispatchTouchEvent(ev)的返回true// 則Activity.dispatchTouchEvent()就返回true皇帮,則方法結(jié)束。即 :該點(diǎn)擊事件停止往下傳遞 & 事件傳遞過程結(jié)束// 否則:繼續(xù)往下調(diào)用Activity.onTouchEvent}// ->>分析4returnonTouchEvent(ev);}/**

? * 分析1:onUserInteraction()

? * 作用:實(shí)現(xiàn)屏保功能

? * 注:

? *? ? a. 該方法為空方法

? *? ? b. 當(dāng)此activity在棧頂時(shí)蛋辈,觸屏點(diǎn)擊按home属拾,back,menu鍵等都會(huì)觸發(fā)此方法

? */publicvoidonUserInteraction(){}// 回到最初的調(diào)用原處/**

? * 分析2:getWindow().superDispatchTouchEvent(ev)

? * 說明:

? *? ? a. getWindow() = 獲取Window類的對(duì)象

? *? ? b. Window類是抽象類冷溶,其唯一實(shí)現(xiàn)類 = PhoneWindow類渐白;即此處的Window類對(duì)象 = PhoneWindow類對(duì)象

? *? ? c. Window類的superDispatchTouchEvent() = 1個(gè)抽象方法,由子類PhoneWindow類實(shí)現(xiàn)

? */@OverridepublicbooleansuperDispatchTouchEvent(MotionEventevent){returnmDecor.superDispatchTouchEvent(event);// mDecor = 頂層View(DecorView)的實(shí)例對(duì)象// ->> 分析3}/**

? * 分析3:mDecor.superDispatchTouchEvent(event)

? * 定義:屬于頂層View(DecorView)

? * 說明:

? *? ? a. DecorView類是PhoneWindow類的一個(gè)內(nèi)部類

? *? ? b. DecorView繼承自FrameLayout逞频,是所有界面的父類

? *? ? c. FrameLayout是ViewGroup的子類纯衍,故DecorView的間接父類 = ViewGroup

? */publicbooleansuperDispatchTouchEvent(MotionEventevent){returnsuper.dispatchTouchEvent(event);// 調(diào)用父類的方法 = ViewGroup的dispatchTouchEvent()// 即 將事件傳遞到ViewGroup去處理,詳細(xì)請(qǐng)看ViewGroup的事件分發(fā)機(jī)制}// 回到最初的調(diào)用原處/**

? * 分析4:Activity.onTouchEvent()

? * 定義:屬于頂層View(DecorView)

? * 說明:

? *? ? a. DecorView類是PhoneWindow類的一個(gè)內(nèi)部類

? *? ? b. DecorView繼承自FrameLayout苗胀,是所有界面的父類

? *? ? c. FrameLayout是ViewGroup的子類襟诸,故DecorView的間接父類 = ViewGroup

? */publicbooleanonTouchEvent(MotionEventevent){// 當(dāng)一個(gè)點(diǎn)擊事件未被Activity下任何一個(gè)View接收 / 處理時(shí)// 應(yīng)用場(chǎng)景:處理發(fā)生在Window邊界外的觸摸事件// ->> 分析5if(mWindow.shouldCloseOnTouch(this,event)){finish();returntrue;}returnfalse;// 即 只有在點(diǎn)擊事件在Window邊界外才會(huì)返回true,一般情況都返回false基协,分析完畢}/**

? * 分析5:mWindow.shouldCloseOnTouch(this, event)

? */publicbooleanshouldCloseOnTouch(Contextcontext,MotionEventevent){// 主要是對(duì)于處理邊界外點(diǎn)擊事件的判斷:是否是DOWN事件歌亲,event的坐標(biāo)是否在邊界內(nèi)等if(mCloseOnTouchOutside&&event.getAction()==MotionEvent.ACTION_DOWN&&isOutOfBounds(context,event)&&peekDecorView()!=null){returntrue;}returnfalse;// 返回true:說明事件在邊界外,即 消費(fèi)事件// 返回false:未消費(fèi)(默認(rèn))}// 回到分析4調(diào)用原處

2.1.2 總結(jié)

當(dāng)一個(gè)點(diǎn)擊事件發(fā)生時(shí)堡掏,從Activity的事件分發(fā)開始(Activity.dispatchTouchEvent())

方法總結(jié)

那么应结,ViewGroup的dispatchTouchEvent()什么時(shí)候返回true/false?請(qǐng)繼續(xù)往下看ViewGroup事件的分發(fā)機(jī)制

2.2 ViewGroup事件的分發(fā)機(jī)制

從上面Activity事件分發(fā)機(jī)制可知泉唁,ViewGroup事件分發(fā)機(jī)制從dispatchTouchEvent()開始

2.2.1 源碼分析

Android 5.0后鹅龄,ViewGroup.dispatchTouchEvent()的源碼發(fā)生了變化(更加復(fù)雜),但原理相同亭畜;

本文為了讓讀者容易理解扮休,故采用Android 5.0前的版本

/**

? * 源碼分析:ViewGroup.dispatchTouchEvent()

? */publicbooleandispatchTouchEvent(MotionEventev){...// 僅貼出關(guān)鍵代碼// 重點(diǎn)分析1:ViewGroup每次事件分發(fā)時(shí),都需調(diào)用onInterceptTouchEvent()詢問是否攔截事件if(disallowIntercept||!onInterceptTouchEvent(ev)){// 判斷值1:disallowIntercept = 是否禁用事件攔截的功能(默認(rèn)是false)拴鸵,可通過調(diào)用requestDisallowInterceptTouchEvent()修改// 判斷值2: !onInterceptTouchEvent(ev) = 對(duì)onInterceptTouchEvent()返回值取反// a. 若在onInterceptTouchEvent()中返回false(即不攔截事件)玷坠,就會(huì)讓第二個(gè)值為true蜗搔,從而進(jìn)入到條件判斷的內(nèi)部// b. 若在onInterceptTouchEvent()中返回true(即攔截事件),就會(huì)讓第二個(gè)值為false八堡,從而跳出了這個(gè)條件判斷// c. 關(guān)于onInterceptTouchEvent() ->>分析1ev.setAction(MotionEvent.ACTION_DOWN);finalintscrolledXInt=(int)scrolledXFloat;finalintscrolledYInt=(int)scrolledYFloat;finalView[]children=mChildren;finalintcount=mChildrenCount;// 重點(diǎn)分析2// 通過for循環(huán)樟凄,遍歷了當(dāng)前ViewGroup下的所有子Viewfor(inti=count-1;i>=0;i--){finalViewchild=children[i];if((child.mViewFlags&VISIBILITY_MASK)==VISIBLE||child.getAnimation()!=null){child.getHitRect(frame);// 判斷當(dāng)前遍歷的View是不是正在點(diǎn)擊的View,從而找到當(dāng)前被點(diǎn)擊的View// 若是兄渺,則進(jìn)入條件判斷內(nèi)部if(frame.contains(scrolledXInt,scrolledYInt)){finalfloatxc=scrolledXFloat-child.mLeft;finalfloatyc=scrolledYFloat-child.mTop;ev.setLocation(xc,yc);child.mPrivateFlags&=~CANCEL_NEXT_UP_EVENT;// 條件判斷的內(nèi)部調(diào)用了該View的dispatchTouchEvent()// 即 實(shí)現(xiàn)了點(diǎn)擊事件從ViewGroup到子View的傳遞(具體請(qǐng)看下面的View事件分發(fā)機(jī)制)if(child.dispatchTouchEvent(ev)){mMotionTarget=child;returntrue;// 調(diào)用子View的dispatchTouchEvent后是有返回值的// 若該控件可點(diǎn)擊缝龄,那么點(diǎn)擊時(shí),dispatchTouchEvent的返回值必定是true挂谍,因此會(huì)導(dǎo)致條件判斷成立// 于是給ViewGroup的dispatchTouchEvent()直接返回了true叔壤,即直接跳出// 即把ViewGroup的點(diǎn)擊事件攔截掉}}}}}}booleanisUpOrCancel=(action==MotionEvent.ACTION_UP)||(action==MotionEvent.ACTION_CANCEL);if(isUpOrCancel){mGroupFlags&=~FLAG_DISALLOW_INTERCEPT;}finalViewtarget=mMotionTarget;// 重點(diǎn)分析3// 若點(diǎn)擊的是空白處(即無任何View接收事件) / 攔截事件(手動(dòng)復(fù)寫onInterceptTouchEvent(),從而讓其返回true)if(target==null){ev.setLocation(xf,yf);if((mPrivateFlags&CANCEL_NEXT_UP_EVENT)!=0){ev.setAction(MotionEvent.ACTION_CANCEL);mPrivateFlags&=~CANCEL_NEXT_UP_EVENT;}returnsuper.dispatchTouchEvent(ev);// 調(diào)用ViewGroup父類的dispatchTouchEvent()口叙,即View.dispatchTouchEvent()// 因此會(huì)執(zhí)行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick()炼绘,即自己處理該事件,事件不會(huì)往下傳遞(具體請(qǐng)參考View事件的分發(fā)機(jī)制中的View.dispatchTouchEvent())// 此處需與上面區(qū)別:子View的dispatchTouchEvent()}...}/**

? * 分析1:ViewGroup.onInterceptTouchEvent()

? * 作用:是否攔截事件

? * 說明:

? *? ? a. 返回true = 攔截妄田,即事件停止往下傳遞(需手動(dòng)設(shè)置俺亮,即復(fù)寫onInterceptTouchEvent(),從而讓其返回true)

? *? ? b. 返回false = 不攔截(默認(rèn))

? */publicbooleanonInterceptTouchEvent(MotionEventev){returnfalse;}// 回到調(diào)用原處

2.2.2 總結(jié)

結(jié)論:Android事件分發(fā)總是先傳遞到ViewGroup形庭、再傳遞到View

過程:當(dāng)點(diǎn)擊了某個(gè)控件時(shí)

核心方法總結(jié)

2.2.3 Demo講解

布局如下

測(cè)試代碼

布局文件:activity_main.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/my_layout"android:layout_width="match_parent"android:layout_height="match_parent"xmlns:app="http://schemas.android.com/apk/res-auto"android:focusableInTouchMode="true"android:orientation="vertical"><Buttonandroid:id="@+id/button1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="按鈕1"/><Buttonandroid:id="@+id/button2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="按鈕2"/></LinearLayout>

核心代碼:MainActivity.java

/**

? * ViewGroup布局(myLayout)中有2個(gè)子View = 2個(gè)按鈕

? */publicclassMainActivityextendsAppCompatActivity{Buttonbutton1,button2;ViewGroupmyLayout;@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);button1=(Button)findViewById(R.id.button1);button2=(Button)findViewById(R.id.button2);myLayout=(LinearLayout)findViewById(R.id.my_layout);// 1.為ViewGroup布局設(shè)置監(jiān)聽事件myLayout.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(Viewv){Log.d("TAG","點(diǎn)擊了ViewGroup");}});// 2\. 為按鈕1設(shè)置監(jiān)聽事件button1.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(Viewv){Log.d("TAG","點(diǎn)擊了button1");}});// 3\. 為按鈕2設(shè)置監(jiān)聽事件button2.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(Viewv){Log.d("TAG","點(diǎn)擊了button2");}});}}

結(jié)果測(cè)試

從上面的測(cè)試結(jié)果發(fā)現(xiàn):

點(diǎn)擊Button時(shí)铅辞,執(zhí)行Button.onClick()厌漂,但ViewGroupLayout注冊(cè)的onTouch()不會(huì)執(zhí)行

只有點(diǎn)擊空白區(qū)域時(shí)萨醒,才會(huì)執(zhí)行ViewGroupLayout的onTouch()

結(jié)論:Button的onClick()將事件消費(fèi)掉了,因此事件不會(huì)再繼續(xù)向下傳遞苇倡。

2.3 View事件的分發(fā)機(jī)制

從上面ViewGroup事件分發(fā)機(jī)制知道富纸,View事件分發(fā)機(jī)制從dispatchTouchEvent()開始

2.3.1 源碼分析

/**

? * 源碼分析:View.dispatchTouchEvent()

? */publicbooleandispatchTouchEvent(MotionEventevent){if(mOnTouchListener!=null&&(mViewFlags&ENABLED_MASK)==ENABLED&&mOnTouchListener.onTouch(this,event)){returntrue;}returnonTouchEvent(event);}// 說明:只有以下3個(gè)條件都為真,dispatchTouchEvent()才返回true旨椒;否則執(zhí)行onTouchEvent()//? ? 1\. mOnTouchListener != null//? ? 2\. (mViewFlags & ENABLED_MASK) == ENABLED//? ? 3\. mOnTouchListener.onTouch(this, event)// 下面對(duì)這3個(gè)條件逐個(gè)分析/**

? * 條件1:mOnTouchListener != null

? * 說明:mOnTouchListener變量在View.setOnTouchListener()方法里賦值

? */publicvoidsetOnTouchListener(OnTouchListenerl){mOnTouchListener=l;// 即只要我們給控件注冊(cè)了Touch事件晓褪,mOnTouchListener就一定被賦值(不為空)}/**

? * 條件2:(mViewFlags & ENABLED_MASK) == ENABLED

? * 說明:

? *? ? a. 該條件是判斷當(dāng)前點(diǎn)擊的控件是否enable

? *? ? b. 由于很多View默認(rèn)enable,故該條件恒定為true

? *//**

? * 條件3:mOnTouchListener.onTouch(this, event)

? * 說明:即 回調(diào)控件注冊(cè)Touch事件時(shí)的onTouch()综慎;需手動(dòng)復(fù)寫設(shè)置涣仿,具體如下(以按鈕Button為例)

? */button.setOnTouchListener(newOnTouchListener(){@OverridepublicbooleanonTouch(Viewv,MotionEventevent){returnfalse;}});// 若在onTouch()返回true,就會(huì)讓上述三個(gè)條件全部成立示惊,從而使得View.dispatchTouchEvent()直接返回true好港,事件分發(fā)結(jié)束// 若在onTouch()返回false,就會(huì)使得上述三個(gè)條件不全部成立米罚,從而使得View.dispatchTouchEvent()中跳出If钧汹,執(zhí)行onTouchEvent(event)

接下來,我們繼續(xù)看:onTouchEvent(event)的源碼分析

詳情請(qǐng)看注釋

Android 5.0后View.onTouchEvent()源碼發(fā)生了變化(更加復(fù)雜)录择,但原理相同拔莱;

本文為了讓讀者更好理解碗降,所以采用Android 5.0前的版本

/**

? * 源碼分析:View.onTouchEvent()

? */publicbooleanonTouchEvent(MotionEventevent){finalintviewFlags=mViewFlags;if((viewFlags&ENABLED_MASK)==DISABLED){return(((viewFlags&CLICKABLE)==CLICKABLE||(viewFlags&LONG_CLICKABLE)==LONG_CLICKABLE));}if(mTouchDelegate!=null){if(mTouchDelegate.onTouchEvent(event)){returntrue;}}// 若該控件可點(diǎn)擊,則進(jìn)入switch判斷中if(((viewFlags&CLICKABLE)==CLICKABLE||(viewFlags&LONG_CLICKABLE)==LONG_CLICKABLE)){switch(event.getAction()){// a. 若當(dāng)前的事件 = 抬起View(主要分析)caseMotionEvent.ACTION_UP:boolean prepressed=(mPrivateFlags&PREPRESSED)!=0;...// 經(jīng)過種種判斷塘秦,此處省略// 執(zhí)行performClick() ->>分析1performClick();break;// b. 若當(dāng)前的事件 = 按下ViewcaseMotionEvent.ACTION_DOWN:if(mPendingCheckForTap==null){mPendingCheckForTap=newCheckForTap();}mPrivateFlags|=PREPRESSED;mHasPerformedLongPress=false;postDelayed(mPendingCheckForTap,ViewConfiguration.getTapTimeout());break;// c. 若當(dāng)前的事件 = 結(jié)束事件(非人為原因)caseMotionEvent.ACTION_CANCEL:mPrivateFlags&=~PRESSED;refreshDrawableState();removeTapCallback();break;// d. 若當(dāng)前的事件 = 滑動(dòng)ViewcaseMotionEvent.ACTION_MOVE:finalintx=(int)event.getX();finalinty=(int)event.getY();intslop=mTouchSlop;if((x<0-slop)||(x>=getWidth()+slop)||(y<0-slop)||(y>=getHeight()+slop)){// Outside button? removeTapCallback();if((mPrivateFlags&PRESSED)!=0){// Remove any future long press/tap checks? removeLongPressCallback();// Need to switch from pressed to not pressed? mPrivateFlags&=~PRESSED;refreshDrawableState();}}break;}// 若該控件可點(diǎn)擊讼渊,就一定返回truereturntrue;}// 若該控件不可點(diǎn)擊,就一定返回falsereturnfalse;}/**

? * 分析1:performClick()

? */publicbooleanperformClick(){if(mOnClickListener!=null){playSoundEffect(SoundEffectConstants.CLICK);mOnClickListener.onClick(this);returntrue;// 只要我們通過setOnClickListener()為控件View注冊(cè)1個(gè)點(diǎn)擊事件// 那么就會(huì)給mOnClickListener變量賦值(即不為空)// 則會(huì)往下回調(diào)onClick() & performClick()返回true}returnfalse;}

2.3.2 總結(jié)

每當(dāng)控件被點(diǎn)擊時(shí):

注:onTouch()的執(zhí)行 先于onClick()

核心方法總結(jié)

2.3.3 Demo講解

下面我將用Demo驗(yàn)證上述的結(jié)論

/**

? * 結(jié)論驗(yàn)證1:在回調(diào)onTouch()里返回false

? */// 1\. 通過OnTouchListener()復(fù)寫onTouch()尊剔,從而手動(dòng)設(shè)置返回falsebutton.setOnTouchListener(newView.OnTouchListener(){@OverridepublicbooleanonTouch(Viewv,MotionEventevent){System.out.println("執(zhí)行了onTouch(), 動(dòng)作是:"+event.getAction());returnfalse;}});// 2\. 通過 OnClickListener()為控件設(shè)置點(diǎn)擊事件精偿,為mOnClickListener變量賦值(即不為空),從而往下回調(diào)onClick()button.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(Viewv){System.out.println("執(zhí)行了onClick()");}});/**

? * 結(jié)論驗(yàn)證2:在回調(diào)onTouch()里返回true

? */// 1\. 通過OnTouchListener()復(fù)寫onTouch()赋兵,從而手動(dòng)設(shè)置返回truebutton.setOnTouchListener(newView.OnTouchListener(){@OverridepublicbooleanonTouch(Viewv,MotionEventevent){System.out.println("執(zhí)行了onTouch(), 動(dòng)作是:"+event.getAction());returntrue;}});// 2\. 通過 OnClickListener()為控件設(shè)置點(diǎn)擊事件笔咽,為mOnClickListener變量賦值(即不為空)// 但由于dispatchTouchEvent()返回true,即事件不再向下傳遞霹期,故不調(diào)用onClick())button.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(Viewv){System.out.println("執(zhí)行了onClick()");}});

測(cè)試結(jié)果

2.4 總結(jié)

若您已經(jīng)看到此處叶组,那么恭喜你,你已經(jīng)能非常熟悉掌握Android的事件分發(fā)機(jī)制了

即:Activity历造、ViewGroup甩十、View的事件分發(fā)機(jī)制

3. 工作流程 總結(jié)

在本節(jié)中,我將結(jié)合源碼吭产,梳理出1個(gè)事件分發(fā)的工作流程總結(jié)侣监,具體如下:

左側(cè)虛線:具備相關(guān)性 & 逐層返回

以角色為核心的圖解說明

以方法為核心的圖解說明

4. 核心方法總結(jié)

已知事件分發(fā)過程的核心方法為:dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()

下面臣淤,我將結(jié)合總結(jié)的工作流程橄霉,再次詳細(xì)講解該3個(gè)方法

4.1 dispatchTouchEvent()

簡(jiǎn)介

返回情況說明

情況1:默認(rèn)

情況2:返回true

情況3:返回false

4.2 onInterceptTouchEvent()

簡(jiǎn)介

注:Activity、View都無該方法

返回情況說明

情況1:true

情況2:false(默認(rèn))

4.3 onTouchEvent()

簡(jiǎn)介

返回情況說明

情況1:返回true

情況2:返回false(default)

4.4 三者關(guān)系

下面邑蒋,我用一段偽代碼來闡述上述3個(gè)方法的關(guān)系 & 事件傳遞規(guī)則

/**

? * 點(diǎn)擊事件產(chǎn)生后

? */// 步驟1:調(diào)用dispatchTouchEvent()publicbooleandispatchTouchEvent(MotionEventev){booleanconsume=false;//代表 是否會(huì)消費(fèi)事件// 步驟2:判斷是否攔截事件if(onInterceptTouchEvent(ev)){// a. 若攔截姓蜂,則將該事件交給當(dāng)前View進(jìn)行處理// 即調(diào)用onTouchEvent ()方法去處理點(diǎn)擊事件consume=onTouchEvent(ev);}else{// b. 若不攔截,則將該事件傳遞到下層// 即 下層元素的dispatchTouchEvent()就會(huì)被調(diào)用医吊,重復(fù)上述過程// 直到點(diǎn)擊事件被最終處理為止consume=child.dispatchTouchEvent(ev);}// 步驟3:最終返回通知 該事件是否被消費(fèi)(接收 & 處理)returnconsume;}

5. 常見的事件分發(fā)場(chǎng)景

下面钱慢,我將通過實(shí)例說明常見的事件傳遞情況 & 流程

5.1 背景描述

討論的布局如下:

情景

用戶先觸摸到屏幕上View C上的某個(gè)點(diǎn)(圖中黃區(qū))

Action_DOWN事件在此處產(chǎn)生

用戶移動(dòng)手指

最后離開屏幕

5.2 一般的事件傳遞情況

一般的事件傳遞場(chǎng)景有:

默認(rèn)情況

處理事件

攔截DOWN事件

攔截后續(xù)事件(MOVE、UP)

場(chǎng)景1:默認(rèn)

即不對(duì)控件里的方法(dispatchTouchEvent()卿堂、onTouchEvent()束莫、onInterceptTouchEvent())進(jìn)行重寫 或 更改返回值

那么調(diào)用的是這3個(gè)方法的默認(rèn)實(shí)現(xiàn):調(diào)用下層的方法 & 逐層返回

事件傳遞情況:(呈U型)

從上往下調(diào)用dispatchTouchEvent()

Activity A ->> ViewGroup B ->> View C

從下往上調(diào)用onTouchEvent()

View C ->> ViewGroup B ->> Activity A

注:雖然ViewGroup B的onInterceptTouchEvent()對(duì)DOWN事件返回了false,但后續(xù)的事件(MOVE草描、UP)依然會(huì)傳遞給它的onInterceptTouchEvent()

這一點(diǎn)與onTouchEvent()的行為是不一樣的:不再傳遞 & 接收該事件列的其他事件

場(chǎng)景2:處理事件

設(shè)View C希望處理該點(diǎn)擊事件览绿,即:設(shè)置View C為可點(diǎn)擊的(Clickable)或 復(fù)寫其onTouchEvent()返回true

最常見的:設(shè)置Button按鈕來響應(yīng)點(diǎn)擊事件

事件傳遞情況:(如下圖)

DOWN事件被傳遞給C的onTouchEvent方法,該方法返回true陶珠,表示處理該事件

因?yàn)閂iew C正在處理該事件挟裂,那么DOWN事件將不再往上傳遞給ViewGroup B 和Activity A的onTouchEvent();

該事件列的其他事件(Move揍诽、Up)也將傳遞給View C的onTouchEvent()

會(huì)逐層往dispatchTouchEvent()返回诀蓉,最終事件分發(fā)結(jié)束

場(chǎng)景3:攔截DOWN事件

假設(shè)ViewGroup B希望處理該點(diǎn)擊事件栗竖,即ViewGroup B復(fù)寫了onInterceptTouchEvent()返回true、onTouchEvent()返回true

事件傳遞情況:(如下圖)

DOWN事件被傳遞給ViewGroup B的onInterceptTouchEvent()渠啤,該方法返回true狐肢,表示攔截該事件,即自己處理該事件(事件不再往下傳遞)

調(diào)用自身的onTouchEvent()處理事件(DOWN事件將不再往上傳遞給Activity A的onTouchEvent())

該事件列的其他事件(Move沥曹、Up)將直接傳遞給ViewGroup B的onTouchEvent()

注:

該事件列的其他事件(Move份名、Up)將不會(huì)再傳遞給ViewGroup B的onInterceptTouchEvent();因:該方法一旦返回一次true妓美,就再也不會(huì)被調(diào)用

逐層往dispatchTouchEvent()返回僵腺,最終事件分發(fā)結(jié)束

場(chǎng)景4:攔截DOWN的后續(xù)事件

結(jié)論

若ViewGroup攔截了一個(gè)半路的事件(如MOVE),該事件將會(huì)被系統(tǒng)變成一個(gè)CANCEL事件 & 傳遞給之前處理該事件的子View壶栋;

該事件不會(huì)再傳遞給ViewGroup的onTouchEvent()

只有再到來的事件才會(huì)傳遞到ViewGroup的onTouchEvent()

場(chǎng)景描述

ViewGroup B無攔截DOWN事件(還是View C來處理DOWN事件)辰如,但它攔截了接下來的MOVE事件

即DOWN事件傳遞到View C的onTouchEvent(),返回了true

實(shí)例講解

在后續(xù)到來的MOVE事件贵试,ViewGroup B的onInterceptTouchEvent()返回true攔截該MOVE事件琉兜,但該事件并沒有傳遞給ViewGroup B;這個(gè)MOVE事件將會(huì)被系統(tǒng)變成一個(gè)CANCEL事件傳遞給View C的onTouchEvent()

后續(xù)又來了一個(gè)MOVE事件毙玻,該MOVE事件才會(huì)直接傳遞給ViewGroup B的onTouchEvent()

后續(xù)事件將直接傳遞給ViewGroup B的onTouchEvent()處理豌蟋,而不會(huì)再傳遞給ViewGroup B的onInterceptTouchEvent(),因該方法一旦返回一次true桑滩,就再也不會(huì)被調(diào)用了梧疲。

View C再也不會(huì)收到該事件列產(chǎn)生的后續(xù)事件

至此,關(guān)于Android常見的事件傳遞情況 & 流程已經(jīng)講解完畢施符。

6. 額外知識(shí)

6.1 Touch事件的后續(xù)事件(MOVE往声、UP)層級(jí)傳遞

若給控件注冊(cè)了Touch事件,每次點(diǎn)擊都會(huì)觸發(fā)一系列action事件(ACTION_DOWN戳吝,ACTION_MOVE,ACTION_UP等)

當(dāng)dispatchTouchEvent()事件分發(fā)時(shí)贯涎,只有前一個(gè)事件(如ACTION_DOWN)返回true听哭,才會(huì)收到后一個(gè)事件(ACTION_MOVE和ACTION_UP)

即如果在執(zhí)行ACTION_DOWN時(shí)返回false,后面一系列的ACTION_MOVE塘雳、ACTION_UP事件都不會(huì)執(zhí)行

從上面對(duì)事件分發(fā)機(jī)制分析知:

dispatchTouchEvent()陆盘、 onTouchEvent() 消費(fèi)事件、終結(jié)事件傳遞(返回true)

而onInterceptTouchEvent 并不能消費(fèi)事件败明,它相當(dāng)于是一個(gè)分叉口起到分流導(dǎo)流的作用隘马,對(duì)后續(xù)的ACTION_MOVE和ACTION_UP事件接收起到非常大的作用

請(qǐng)記住:接收了ACTION_DOWN事件的函數(shù)不一定能收到后續(xù)事件(ACTION_MOVE妻顶、ACTION_UP)

這里給出ACTION_MOVE和ACTION_UP事件的傳遞結(jié)論

結(jié)論1

若對(duì)象(Activity酸员、ViewGroup蜒车、View)的dispatchTouchEvent()分發(fā)事件后消費(fèi)了事件(返回true),那么收到ACTION_DOWN的函數(shù)也能收到ACTION_MOVE和ACTION_UP

黑線:ACTION_DOWN事件傳遞方向

紅線:ACTION_MOVE 幔嗦、 ACTION_UP事件傳遞方向

結(jié)論2

若對(duì)象(Activity酿愧、ViewGroup、View)的onTouchEvent()處理了事件(返回true)邀泉,那么ACTION_MOVE嬉挡、ACTION_UP的事件從上往下傳到該View后就不再往下傳遞,而是直接傳給自己的onTouchEvent()& 結(jié)束本次事件傳遞過程汇恤。

黑線:ACTION_DOWN事件傳遞方向

紅線:ACTION_MOVE庞钢、ACTION_UP事件傳遞方向

6.2 onTouch()和onTouchEvent()的區(qū)別

該2個(gè)方法都是在View.dispatchTouchEvent()中調(diào)用

但onTouch()優(yōu)先于onTouchEvent執(zhí)行;若手動(dòng)復(fù)寫在onTouch()中返回true(即 將事件消費(fèi)掉)因谎,將不會(huì)再執(zhí)行onTouchEvent()

注:若1個(gè)控件不可點(diǎn)擊(即非enable)焊夸,那么給它注冊(cè)onTouch事件將永遠(yuǎn)得不到執(zhí)行,具體原因看如下代碼

// &&為短路與蓝角,即如果前面條件為false阱穗,將不再往下執(zhí)行//? 故:onTouch()能夠得到執(zhí)行需2個(gè)前提條件:// 1\. mOnTouchListener的值不能為空// 2\. 當(dāng)前點(diǎn)擊的控件必須是enable的mOnTouchListener!=null&&(mViewFlags&ENABLED_MASK)==ENABLED&&mOnTouchListener.onTouch(this,event)// 對(duì)于該類控件,若需監(jiān)聽它的touch事件使鹅,就必須通過在該控件中重寫onTouchEvent()來實(shí)現(xiàn)

7. 總結(jié)

通過閱讀本文叨粘,相信您已經(jīng)可以全面了解Android事件分發(fā)機(jī)制

與Android事件分發(fā)最相關(guān)的知識(shí):自定義View系列文章

(1)自定義View基礎(chǔ) - 最易懂的自定義View原理系列

(2)自定義View Measure過程 - 最易懂的自定義View原理系列

(3)自定義View Layout過程 - 最易懂的自定義View原理系列

(4)自定義View Draw過程- 最易懂的自定義View原理系列

接下來我將繼續(xù)介紹與Android事件分發(fā)最相關(guān)的知識(shí):自定義View,感興趣的同學(xué)可以繼續(xù)關(guān)注本人運(yùn)營(yíng)的Wechat Public Account:

我想給你們介紹一個(gè)與眾不同的Android微信公眾號(hào)(福利回贈(zèng))

我想邀請(qǐng)您和我一起寫Android(福利回贈(zèng))

請(qǐng)點(diǎn)贊苍姜!因?yàn)槟銈兊馁澩?鼓勵(lì)是我寫作的最大動(dòng)力旭斥!

相關(guān)文章閱讀

Android開發(fā):最全面、最易懂的Android屏幕適配解決方案

Android開發(fā):史上最全的Android消息推送解決方案

Android開發(fā):最全面裁厅、最易懂的Webview詳解

Android開發(fā):JSON簡(jiǎn)介及最全面解析方法!

Android四大組件:Service服務(wù)史上最全面解析

Android四大組件:BroadcastReceiver史上最全面解析

作者:Carson_Ho

鏈接:http://www.reibang.com/p/38015afcdb58

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末冰沙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子执虹,更是在濱河造成了極大的恐慌拓挥,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件袋励,死亡現(xiàn)場(chǎng)離奇詭異侥啤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)茬故,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門盖灸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人磺芭,你說我怎么就攤上這事赁炎。” “怎么了钾腺?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵徙垫,是天一觀的道長(zhǎng)讥裤。 經(jīng)常有香客問我,道長(zhǎng)松邪,這世上最難降的妖魔是什么坞琴? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮逗抑,結(jié)果婚禮上剧辐,老公的妹妹穿的比我還像新娘。我一直安慰自己邮府,他們只是感情好荧关,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著褂傀,像睡著了一般忍啤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仙辟,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天同波,我揣著相機(jī)與錄音,去河邊找鬼叠国。 笑死未檩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的粟焊。 我是一名探鬼主播冤狡,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼项棠!你這毒婦竟也來了悲雳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤香追,失蹤者是張志新(化名)和其女友劉穎合瓢,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翅阵,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歪玲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掷匠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡岖圈,死狀恐怖讹语,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蜂科,我是刑警寧澤顽决,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布短条,位于F島的核電站,受9級(jí)特大地震影響才菠,放射性物質(zhì)發(fā)生泄漏茸时。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一赋访、第九天 我趴在偏房一處隱蔽的房頂上張望可都。 院中可真熱鬧,春花似錦蚓耽、人聲如沸渠牲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽签杈。三九已至,卻和暖如春鼎兽,著一層夾襖步出監(jiān)牢的瞬間答姥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工谚咬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鹦付,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓序宦,卻偏偏與公主長(zhǎng)得像睁壁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子互捌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354