為了寫這篇文章斥难,我反復(fù)的看了好幾十遍源碼。而且寫的時候時間間隔比較長帘饶,有時候?qū)懼鴮懼约憾蓟靵y了哑诊,又去看一遍源碼去分析,所以可能會重復(fù)的內(nèi)容比較多也會稍微亂一點及刻,不過我相信你跟著源碼和這邊文章一步一步走镀裤,應(yīng)該還是會有收獲的!
本片文章將會介紹缴饭,view事件是怎么傳遞的和分發(fā)的暑劝,以及點擊滑動沖突產(chǎn)生的原因和解決辦法。這些都會通過閱讀源碼解決~
一些基礎(chǔ)的知識
MotionEvent
當(dāng)手指接觸屏幕時颗搂,會先觸發(fā)ActionDown一次担猛,然后會觸發(fā)一次或多次ActionMove,最后觸發(fā)一次ActionUp
事件分發(fā),攔截丢氢,消費
這是三個方法分別在activity傅联,viewGroup,View中的存在狀況
本片文章也是一直圍繞這三個方法做不同情況的解讀
簡陋事件分發(fā)圖
這里堆疊的是一個個view疚察,因為view都是一個個堆疊在屏幕上的
onTouch 和 onClick
首先看一段簡單的代碼蒸走,給button設(shè)置onTouch和onClick事件。我們可以知道貌嫡,
當(dāng)onTouch事件 return false時比驻,onClick 會執(zhí)行
當(dāng)onTouch事件 return true時,onClick 不會執(zhí)行
結(jié)果我們都知道岛抄,也知道是onTouch攔截了才不會執(zhí)行onClick别惦。
但是這兩段簡單的代碼在源碼中是怎么體現(xiàn)的呢?
因為Button屬于一個View弦撩,所有我們直接進入view的源碼步咪,
看他的dispatchTouchEvent事件分發(fā)的方法,
可以知道第一個if判斷的結(jié)果益楼,是會影響第二個if語句的執(zhí)行的
首先在第一個if看到了我們的onTouch事件
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
我們把if條件里的判斷拆解一下
1.li != null && li.mOnTouchListener != null
首先可以看到 li = mLisenterInfo猾漫,mLisenterInfo是什么呢?
我們回設(shè)置onTouch事件調(diào)用的setOnTouchListener方法感凤,進入查看悯周,可以看到getListenerInfo()
接著再進入getListenerInfo()查看
通過這兩幅圖,我們可以知道的是
li = mLisenterInfo != null陪竿,
li.mOnTouchListener !=null(在setOnTouchListener賦值了我們傳入的值)
所以第一個條件是成立的
2.(mViewFlags & ENABLED_MASK) == ENABLED
這個條件不用做過多的解讀禽翼,就是判斷能不能點擊
所以第二個條件是成立的
3.li.mOnTouchListener.onTouch(this, event)
這調(diào)用的方法就是我們給button設(shè)置的onTouch了
li.mOnTouchListener.onTouch(this, event) return false ---> result = false
return true ---> result = true
// 第二個if語句
if (!result && onTouchEvent(event)) { }
3.1 假設(shè)我們 return false屠橄,那么第一個if語句就失效了不能進入,result還是初始值fasle
所以我們會執(zhí)行第二個if ---> 執(zhí)行onTouchEvent(event)闰挡,事件消費
接著進入performClick锐墙,最終可以看到我們的onClick方法的調(diào)用
3.2 假設(shè)我們 return true,那么第一個if語句就失效了能進入长酗,result被賦值為true
所以我們不會執(zhí)行第二個if ---> 也就不能執(zhí)行事件消費onTouchEvent(event)溪北,也就不能執(zhí)行到onClick方法了
再從事件分發(fā)流程的角度來看
首先進入viewGroup#dispatchTouchEvent,分析一個正常的Down事件
注意:if(!canceled && !intercepted){}
這一個if語句里面的代碼塊夺脾,全都是與事件分發(fā)相關(guān)的之拨,可以說只要進入了一個if語句,就會執(zhí)行事件分發(fā)咧叭,接下來也是對if語句里面的代碼進行分析
題外話
這里我們先來看2注釋中buildTouchDispatchChildList方法
它最后會執(zhí)行到buildOrderedChildList
里面將所有childView按照Z軸的大小蚀乔,從小到大排序,
最小的在最前面菲茬,最大的在后面
Z軸是怎么來的呢吉挣,我們知道一個layout布局里面的所有View都是一個個疊加上去的就像這樣,所以最底層的Z軸越小婉弹,越排在列表的前面听想。所以遍歷的時候,也是從最后一個拿的
再看一下4注釋中isTransformedTouchPointInView方法
比如你點擊的是圖中的小圓圈马胧,當(dāng)他遍歷button1時,就會根據(jù)你點擊的坐標(biāo)和button1的區(qū)域做比較衔峰,看看是不是在自己的范圍內(nèi)佩脊,不是的話continue繼續(xù)遍歷下一個childview
再看一下4注釋下面的方法newTouchTarget = getTouchTarget(child);
題外話結(jié)束
繼續(xù)往下走的話,會進入到dispatchTransformedTouchEvent
因為child != null 進入else語句垫卤,接著就會調(diào)用child.dispatchTouchEvent(transformedEvent)
在上述案例中威彰,button就是child,所以事件就這樣分發(fā)給了button去做后續(xù)操作
button中沒有重寫dispatchTouchEvent穴肘,所以就進入View的dispatchTouchEvent
也就是回到我們一開始分析的結(jié)果了
可以看到歇盼,如果button處理或者消費事件或者在onTouch返回true(也算是處理),就會返回true评抚。進而child.dispatchTouchEvent(transformedEvent)的結(jié)果就是true
接著回到之前的方法豹缀,if語句會被命中,接著會進入
而if語句最后會break掉慨代,就直接退出了本次for循環(huán)了邢笙,本次事件就被button處理了,也不會被其他或者父view獲取了侍匙,接著就會開始下一個view或者viewgroup了的時間分發(fā)了氮惯。
另外這里有兩個紅框的語句,會得到三個條件,特別注意一下
newTouchTarget = mFirstTouchTarget != null
mFirstTouchTarget.next = null
alreadyDispatchedToNewTouchTarget = true
這里把之前的代碼折疊起來了妇汗,為了方便看帘不。
最后的語句因為上面的上面的條件而不會命中,
最后整個if (!canceled && !intercepted)
里的代碼塊就結(jié)束了
接著往下走杨箭,因為mFirstTouchTarget != null 所以我們來看else語句的代碼塊
最后將handled的結(jié)果return寞焙,dispatchTouchEvent方法結(jié)束
這里Down事件結(jié)束
滑動沖突
上面只是正常的down事件分發(fā)
接下來用這個例子來看一下有沖突的事件分發(fā)來分析一下down和move事件
先介紹一下情況,布局是這樣的:
自定義了一個BadViewpager告唆,里面放著一個listview棺弊,listview里面有很多item,超過一屏幕
所以這里viewpager是父view擒悬,listview就是子view
BadViewpager正常情況下是可以左右滑動的
listview正常情況下是可以上下滑動的
BadViewpager模她,重寫了onInterceptTouchEvent
攔截事件的方法,并且返回true(為了制造沖突)
如果 BadViewpager的onInterceptTouchEvent返回ture,攔截事件
此時viewpager是可以左右滑動的懂牧,但是listview不能上下滑動
也就是說事件分發(fā)到viewpager就被攔截了侈净,讓我們來看看viewpager是怎么攔截事件,并且自己消費事件的僧凤。
我們從頭開始畜侦,回到ViewGroup的dispatchTouchEvent
注意:現(xiàn)在是ACTION_DOWN事件
因為在viewpager重寫了onInterceptTouchEvent方法,導(dǎo)致intercepted的為true
我們知道if (!canceled && !intercepted) {}
是將事件分發(fā)給子View的關(guān)鍵代碼塊
intercepted是true 就表示if語句不能進入躯保,就不能將事件分發(fā)給子View(也就是listview)
而且mFirstTouchTarget是在if (!canceled && !intercepted) {}
里面賦值的旋膳,所以往下走的話
注意這里傳入的child是null,因為沒有取消事件所以canceled為false
所以進入dispatchTransformedTouchEvent后我們可以看到
這里直接調(diào)用了自己的dispatchTouchEvent途事,就把事件分發(fā)給自己的
這里ACTION_DOWN事件就結(jié)束了
如果 BadViewpager的onInterceptTouchEvent返回false,不攔截事件
此時viewpager是不可以左右滑動的验懊,但是listview能上下滑動
ACTION_DOWN事件流程和上面button的事件分發(fā)情況是一樣的,就不分析了尸变。
所以這里是分發(fā)了一次ACTION_DOWN事件后义图,
再次執(zhí)行dispatchTouchEvent分發(fā)ACTION_MOVE。
所以有幾個條件要注意
newTouchTarget = mFirstTouchTarget != null
mFirstTouchTarget.next = null
alreadyDispatchedToNewTouchTarget = false
(注意這里有不同)
因為每次執(zhí)行dispatchTouchEvent召烂,
alreadyDispatchedToNewTouchTarget 都會被重置
而alreadyDispatchedToNewTouchTarget 字段只有在分發(fā)子view時才會被賦值為true
但是根據(jù)下圖的判斷碱工,在move事件中是不會執(zhí)行下面的語句的
這里ACTION_MOVE事件
所以我們接分析下面的else語句
進入dispatchTransformedTouchEvent我們可以看到
這里ACTION_MOVE事件結(jié)束
內(nèi)部攔截和外部攔截
先來看一下內(nèi)部攔截
注意內(nèi)部攔截是子view和父view都要進行處理的
在子view(listview)中
在父view(viewpager)中
首先來看一下getParent().requestDisallowInterceptTouchEvent()
傳入true時 mGroupFlags | FLAG_DISALLOW_INTERCEPT
傳入false時 mGroupFlags & FLAG_DISALLOW_INTERCEPT
所以到dispatchTouchEvent時(mGroupFlags & FLAG_DISALLOW_INTERCEPT)
傳入true時,運算結(jié)果就是 !=0奏夫,disallowIntercept為true怕篷,intercepted為false,后續(xù)事件會正常分發(fā)
傳入false時酗昼,運算結(jié)果就是 =0匙头,disallowIntercept為false,intercepted根據(jù)onInterceptTouchEvent情況定
接下來再分析一下仔雷,為什么要再父view做處理蹂析,而不是直接返回true就行了
最簡單的:不做處理返回true 子view就根本不會接收到事件舔示。內(nèi)部攔截的代碼都不會執(zhí)行。我認(rèn)為這只是其中一個原因
根據(jù)這個案例來說电抚,viewgroup分發(fā)事件給listview惕稻,listview也是一個ViewGroup。
所以它也會走ViewGroup的dispatchTouchEvent蝙叛,這個時候問題就來了俺祠。
在分發(fā)ACTION_DOWN,會執(zhí)行一個重置方法
這邊將mGroupFlags 做了運算
再回到dispatchTouchEvent方法中時借帘,mGroup又做了運算蜘渣,
所以最終的值就是這樣一個操作
mGroupFlags & ~FLAG_DISALLOW_INTERCEPT &FLAG_DISALLOW_INTERCEPT
,導(dǎo)致這個值肯定為0
所以disallowIntercept為false肺然,直接進入下面的if語句
此時如果沒在父view對onInterceptTouchEvent的down事件做處理的話蔫缸,返回true
那么intercepted就會為true
cancel事件的產(chǎn)生
還是繼續(xù)用這個案例,還是用內(nèi)部攔截去分析际起,當(dāng)down事件結(jié)束后拾碌,我們會進入到move事件
首先move事件進來時,是listview拿著這個事件街望,他會執(zhí)行他自己的這個方法
由于上面知道傳入false會導(dǎo)致disallowIntercept為false校翔,intercepted根據(jù)onInterceptTouchEvent情況定
而此時的ViewGroup返回是true,所以intercepted為true灾前,就會導(dǎo)致如下的執(zhí)行
執(zhí)行dispatchTransformedTouchEvent 并且下面會對mFirstTouchTarget賦值
前面已經(jīng)分析過了防症,next就是null,所以這里是將mFirstTouchTarget賦值為null
進入dispatchTransformedTouchEvent查看一下哎甲,
我們可以知道告希,這里是取消掉子view的事件的
這個move事件執(zhí)行完后,接著還是move事件烧给,因為他是觸發(fā)多次的,
這里就會直接走mFirstTouchTarget == null 的判定了喝噪,這時就是父view拿到事件了
所以就可以從listview的上下滑動轉(zhuǎn)換到viewpager的左右滑動
結(jié)尾
外部攔截的流程就不分析了础嫡,分析下來其實和cancel事件產(chǎn)生的流程大同小異,就不做重復(fù)了酝惧。