欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android View 事件分發(fā)機(jī)制詳解

 更新時間:2016年08月31日 10:14:51   作者:ITKobe24  
本文主要介紹Android View 事件分發(fā)機(jī)制,這里整理了相關(guān)資料并詳細(xì)介紹了view分發(fā)機(jī)制的知識及簡單示例代碼,有興趣的小伙伴可以參考下

Android開發(fā),觸控?zé)o處不在。對于一些 不咋看源碼的同學(xué)來說,多少對這塊都會有一些疑惑。View事件的分發(fā)機(jī)制,不僅在做業(yè)務(wù)需求中會碰到這些問題,在一些面試筆試題中也常有人問,可謂是老生常談了。我以前也看過很多人寫的這方面的文章,不是說的太啰嗦就是太模糊,還有一些在細(xì)節(jié)上寫的也有爭議,故再次重新整理一下這塊內(nèi)容,十分鐘讓你搞明白View事件的分發(fā)機(jī)制。

說白了這些觸控的事件分發(fā)機(jī)制就是弄清楚三個方法,dispatchTouchEvent(),OnInterceptTouchEvent(),onTouchEvent(),和這三個方法與n個ViewGroup和View堆疊在一起的問題,再復(fù)雜的結(jié)構(gòu)都能拆分成1個ViewGroup+1個View。

其實ViewGroup和View都是大同小異,View只是沒有了子容器,自然不存在攔截問題,dispatch也很簡單,所以弄明白了ViewGroup其實就懂的差不多了。

三個關(guān)鍵方法

public boolean dispatchTouchEvent(MotionEvent ev)

View/ViewGroup處理事件分發(fā)的發(fā)起者,View/ViewGroup接收到觸控事件最先調(diào)起的就是這個方法,然后在該方法中判斷是否處理攔截或是將事件分發(fā)給子容器

public boolean onInterceptTouchEvent(MotionEvent ev)

ViewGroup專用,通過該方法可以達(dá)到控件事件的分發(fā)方向,一般可以在該方法中判斷將事件給ViewGroup獨(dú)吞或是它繼續(xù)傳遞給子容器,是處理事件沖突的最佳地點(diǎn)

public boolean onTouchEvent(MotionEvent event)

觸控事件的真正處理者,最后每個事件都會在這里被處理

核心問題

時間分發(fā)機(jī)制的難點(diǎn)在哪,我覺得難的地方以下幾點(diǎn):三個方法調(diào)用規(guī)則,確定處理事件的對象以及事件沖突的解決方法。

事件傳遞規(guī)則

一般一次點(diǎn)擊會有一系列的MotionEvent,可以簡單分為:down->move->….->move->up,當(dāng)一次event分發(fā)到ViewGroup時,上述三個方法之間的 ViewGroup中調(diào)用順序可以用一段簡單代碼表示

MotionEvent ev;//down or move or up or others...
viewgroup.dispatchTouchEvent(ev);

public boolean dispatchTouchEvent(MotionEvent ev){
 boolean isConsumed = false;
  if(onInterceptTouchEvent(ev)){
   isCousumed = this.onTouchEvent(ev);
  }else{
   isConsumed = childView.dispatchTouchEvent(ev);
  }
  return isConsumed;
}

返回結(jié)果true表示事件被處理了,返回false表示沒有處理。上面的代碼通俗易懂,看起來也很簡單,一句話就能概括,ViewGroup收到事件后調(diào)用dispatch,在dispatch中先檢查是否要攔截,若攔截則ViewGroup吃掉事件,否則交給有處理能力的子容器處理。

不過,簡單歸簡單,寫成這樣只是為了方便理解,ViewGroup的事件處理流程當(dāng)然沒這么簡單,這里忽略了很多細(xì)節(jié)問題,接下來繼續(xù)補(bǔ)充。回到上面說的,一系列事件我們經(jīng)常處理的一般都是一個down,多個move和一個up,光靠上面的偽代碼是沒辦法把這些問題都給完美解決,直接來看ViewGroup的dispatchTouchEvent。

onInterceptTouchEvent調(diào)用條件

final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
    || mFirstTouchTarget != null) {
  final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
  if (!disallowIntercept) {
    intercepted = onInterceptTouchEvent(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;
}

解釋一下上面的代碼,看起來好像很簡單,但真的很簡單嗎。。在解釋之前先說一下intercepted代表的含義,intercepted == false表示父容器ViewGroup暫時不攔截事件,事件有機(jī)會傳給子View處理,返回true表示父容器直接攔截了該系列事件,后續(xù)不會再傳遞給子View了。子View想獲取事件只能讓該值為false

onInterceptTouchEvent調(diào)用返回false(返回false才能傳遞給子View,對應(yīng)到上面?zhèn)未a的else中的內(nèi)容,叫事件傳遞到子容器需要滿足的內(nèi)容更好理解一些)需要滿足兩個條件中的任意一個就有可能觸發(fā)(當(dāng)然只是有可能):

一個是在down的時候,另一個就是mFirstTouchTarget!=null,那mFirstTouchTarget何時不為空,有興趣的同學(xué)可以看ViewGroup中的addTouchTarget這個方法的調(diào)用時機(jī),mFirstTouchTarget就是在這里賦值的,源碼太長我就不貼了。

mFirstTouchTarget是用來保存ViewGroup中消費(fèi)了ACTION_DOWN事件的子View,即在上面?zhèn)未a中child.dispatchTouchEvent(ev)在ACTION_DOWN的時候返回true的View,只要有子View的dispatch在ACTION_DOWN返回true,就不會為null(這個賦值過程只發(fā)生在ACTION_DOWN里,如果子ViewACTION__DOWN不給它賦值后面序列的事件就不會再),反之,若無子View處理,該對象即為null。當(dāng)然,滿足了上述兩個條件還不行,必須還要滿足!disallowIntercept。

disallowIntercept這個變量很有意思,它的值主要受FLAG_DISALLOW_INTERCEPT這個標(biāo)記影響,這個值可以被ViewGroup的子View設(shè)置,ViewGroup的子View如果調(diào)用了requestDisallowInterceptTouchEvent這個方法,會改變FLAG_DISALLOW_INTERCEPT,導(dǎo)致disallowIntercept這個值就是ture了,這種情況會跳過intercept,導(dǎo)致攔截失效。

但這事還沒了,F(xiàn)LAG_DISALLOW_INTERCEPT這個標(biāo)記有一個重置的機(jī)制,查看ViewGroup源碼可以看到,在處理MotionEvent.ACTION_DOWN的時候會重置這個標(biāo)記導(dǎo)致disallowIntercept失效,是不是喪心病狂,上面的一段這么簡單的代碼有這么多幺蛾子,這里還能得到一個結(jié)論,ACTION_DOWN的時候肯定可以執(zhí)行onInterceptTouchEvent的。

所以攔截的intercepted很重要,能影響到底是讓ViewGroup還子View處理這個事件。

上面的兩個有可能觸發(fā)攔截的條件說完了,那么當(dāng)兩個條件都不滿足的話就不會再調(diào)用攔截了(攔截很重要,一般ViewGroup都返回false這樣能把事件傳遞給子View,如果在ACTION_DOWN時不能走到OnInterceptTouchEvent并返回false告訴ViewGroup不要攔截,則事件再也不能傳給子View了,所以攔截一般都是要走到的,而且一般都是返回false這樣能讓子View有機(jī)會處理),這種情況一般都是在ACTION_DOWN處理完之后沒有子View當(dāng)接盤俠消費(fèi)ACTION_DOWN以及后續(xù)事件,從上面的偽代碼可以看出來,這時候ViewGroup自己就很被動了,需要自己來調(diào)用onTouchEvent來處理,這鍋就自己背了。

再繼續(xù)說一下mFirstTouchTarget和intercepted是怎么影響事件方向的??丛创a:

if (!canceled && !intercepted) {
....
if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
 ....
 for(child : childList){
   if(!child satisfied condition....){
     continue;
   }
   newTouchTarget = addTouchTarget(child, idBitsToAssign);//在這里給mFirstTouchTarget賦值
 }

 }
}

可以在這里看到intercepted為false在ACTION_DOWN里才能給上面說過的mFirstTouchTarget賦值,只有mFirstTouchTarget不為空才能讓后續(xù)事件傳遞給子View,否則根據(jù)上上面說的代碼后續(xù)事件只能給父容器處理了。

mFirstTouchTarget就是我們后續(xù)事件傳遞的對象,很容易理解,如果在ACTION_DOWN中沒有確定這個對象,則后續(xù)事件不知道傳遞給誰自然就交給父容器ViewGroup處理了,真正處理事件傳遞的方法是dispatchTransformedTouchEvent,再看源碼:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
      View child, int desiredPointerIdBits) {
   final boolean handled;

    // Canceling motions is a special case. We don't need to perform any transformations
    // or filtering. The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
      event.setAction(MotionEvent.ACTION_CANCEL);
      if (child == null) {
        handled = super.dispatchTouchEvent(event);
      } else {
        handled = child.dispatchTouchEvent(event);
      }
      event.setAction(oldAction);
      return handled;
    }

}

看到?jīng)],只要參數(shù)里傳的child為空,則ViewGroup調(diào)用super.dispatchTouchEvent(event),super是誰,ViewGroup繼承自View,當(dāng)然是View咯,View的dispatch調(diào)用的誰?當(dāng)然是自己的onTouchEvent(后面會說),所以這個最后還是調(diào)用了ViewGroup自己的onTouchEvent。

那么當(dāng)child!=null的時候呢,調(diào)用的是child的dispatchTouchEvent(event),如果child可能是View也可能是ViewGroup,如果是ViewGroup則繼續(xù)按照上面的偽代碼執(zhí)行事件分發(fā),如果也是View則調(diào)用自己的onTouchEvent。

所以,說到底事件到底給誰處理,還是和傳進(jìn)來的child有關(guān),那這個方法在哪里調(diào)用的呢,繼續(xù)看:

if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
      } else {
     ...
     dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)
   }

這就是為什么mFirstTouchTarget能影響事件分發(fā)的方向的原因。就這樣,整個偽代碼的流程是不是很清楚了。

這里需要多說兩句,在上上面代碼流程中,intercepted決定了這個事件會不會調(diào)用ViewGroup的onTouchEvent,當(dāng)intercepted為true則后續(xù)流程會調(diào)用ViewGroup的onTouchEvent,仔細(xì)看上面的代碼能發(fā)現(xiàn),只有兩種情況為ture:一是調(diào)用了InterceptTouchEvent把事件攔截下來,另一個就是沒有一個子View能夠消費(fèi)ActionDown。只有這兩種情況父容器ViewGroup才會自己處理
那么問題來了,思考一個問題:如果子View處理了ACTION_DOWN但后續(xù)事件都返回false,這些沒有被處理的事件最后傳給誰處理了?各位思考之,后面再說這個問題。

孩子是誰的

繼續(xù)來擴(kuò)展我們的偽代碼,攔截條件判斷完之后,決定把事件繼續(xù)傳遞給子View的時候,會調(diào)用childView.dispatchTouchEvent(ev),問題來了,child是哪來的,繼續(xù)看源碼

if (!canViewReceivePointerEvents(child)
  || !isTransformedTouchPointInView(x, y, child, null)) {
   ev.setTargetAccessibilityFocus(false);
   continue;
}

ViewGroup通過判斷所有的子View是否可見是否在播放動畫和是否在點(diǎn)擊范圍內(nèi)來決定它是否能夠有資格接受事件。只有滿足條件的child才能夠調(diào)用dispatch。

再看偽代碼,最后dispatch返回ViewGroup的isConsumed,若isConsume == true,說明ViewGroup處理了這個點(diǎn)擊事件(ViewGroup自身或者子View處理的),并且這個系列的點(diǎn)擊事件會繼續(xù)傳到這個ViewGroup來處理,若isConsume == false(ACTION_DOWN時),ViewGroup沒辦法處理這個點(diǎn)擊事件,那么這個系類的點(diǎn)擊事件就和該ViewGroup無緣了。會把這個事件上拋給自己的父容器或者Activity處理。

偽代碼說完了,ViewGroup的事件傳遞規(guī)則也就差不多說完了,這么看是不是很簡單了。View相對于ViewGroup來說就更簡單了,沒有攔截方法,dispatch基本上是直接調(diào)用了自身的onTouchEvent,處理起來一點(diǎn)難度都木有呀。

一些沒說到但也很重要的點(diǎn)

上面解釋的東西都很簡單,是從一個ViewGroup+一個View開始的,事件分發(fā)的執(zhí)行者是ViewGroup,子容器也只有一個View,但實際開發(fā)中當(dāng)然沒這么簡單,不過不要怕,再復(fù)雜的情況也能夠拆分成這種模式的,只不過層次多了一些遞歸復(fù)雜了一些而已,原理還是一樣的。

順帶補(bǔ)充幾點(diǎn):

從用戶點(diǎn)擊屏幕開始觸發(fā)一個系列的點(diǎn)擊事件時,事件真正的傳遞流程是:Activity(PhoneWindow)->DecorView->ViewGroup->View,在到達(dá)ViewGroup之前還有一個DecorView,事件是從Activity傳過來的,但這些東西其實和ViewGroup的原理是一樣的,Activity能看做一個大的ViewGroup,當(dāng)它的DecorView包含的所有子View沒有人能夠消耗事件的時候(這樣說有漏洞,大家懂我的意思就行了)最后還是會交給Activity處理。

事件沖突解決可以按照上面的原理在幾個point中進(jìn)行處理。最容易想到的處理的時機(jī)是在onInterceptTouchEvent里,比如當(dāng)一個豎直方向滑動的ViewGroup里嵌套一個橫向滑動的ViewGroup,可以在這里的ACTION_MOVE里來判斷后續(xù)事件應(yīng)該傳遞給誰處理,當(dāng)然,也可以根據(jù)上面說的標(biāo)記位FLAG_DISALLOW_INTERCEPT配合子View的dispatchTouchEvent來控制事件的流向,這都是比較容易想到的,不過看過別的大神,通過分享MotionEvent的方法來控制事件的流向,即在父容器中保存MotionEvent并在適當(dāng)?shù)臅r機(jī)傳入子View自定義的事件處理方法來分享事件,也是可行的。

任何View只要拒絕了一系列事件中的ACTION_DOWN(返回false),則后續(xù)事件都不會再傳遞過來了。但如果拒絕了其他的事件,后續(xù)事件還是可以傳過來的,比如View某次ACTION_MOVE沒處理,這個沒處理的事件最后會被Activity消耗掉(而不是View的父容器),但后續(xù)的事件還是會繼續(xù)傳給該View。

合理的利用ACTION_CANCEL能夠控制一個系列事件的生命周期,讓事件處理更加靈活。

理解事件分發(fā)的機(jī)制只要明白上面的原理基本就夠用了,github上很多牛逼的大神寫的各種炫酷的自定義控件的事分發(fā)根據(jù)這些也能夠看明白,當(dāng)然還有很多擴(kuò)展的東西和更深入的內(nèi)容由于篇幅的關(guān)系在這里就不羅嗦了,更重要的還是去看源碼吧。
最后送各位一句經(jīng)典:紙上得來終覺淺,絕知此事要躬行!

以上就是對Android View事件分發(fā)機(jī)制的資料整理,后續(xù)繼續(xù)補(bǔ)充相關(guān)資料,謝謝大家對本站的支持!

相關(guān)文章

  • eclipse中運(yùn)行monkeyrunner腳本之環(huán)境搭建(4)

    eclipse中運(yùn)行monkeyrunner腳本之環(huán)境搭建(4)

    這篇文章主要為大家詳細(xì)介紹了eclipse中運(yùn)行monkeyrunner腳本之環(huán)境搭建的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-12-12
  • 去除arraylist容器中的相同的對象元素的方法

    去除arraylist容器中的相同的對象元素的方法

    下面小編就為大家?guī)硪黄コ齛rraylist容器中的相同的對象元素的方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-09-09
  • Eclipse安裝ADT插件速度慢的解決方法

    Eclipse安裝ADT插件速度慢的解決方法

    這篇文章主要介紹了Eclipse安裝ADT插件速度慢的解決方法,涉及基于Apache的反向代理與hosts文件的設(shè)置方法,需要的朋友可以參考下
    2015-12-12
  • android 實現(xiàn)ScrollView自動滾動的實例代碼

    android 實現(xiàn)ScrollView自動滾動的實例代碼

    這篇文章主要介紹了android 實現(xiàn)ScrollView自動滾動的實例代碼,有需要的朋友可以參考一下
    2014-01-01
  • Android RecycleView添加head配置封裝的實例

    Android RecycleView添加head配置封裝的實例

    這篇文章主要介紹了Android RecycleView添加head配置封裝的實例的相關(guān)資料,這里提供實例幫助大家實現(xiàn)這樣的功能,需要的朋友可以參考下
    2017-08-08
  • Android編程實現(xiàn)TCP客戶端的方法

    Android編程實現(xiàn)TCP客戶端的方法

    這篇文章主要介紹了Android編程實現(xiàn)TCP客戶端的方法,結(jié)合實例形式分析了Android實現(xiàn)TCP客戶端的原理及數(shù)據(jù)通信的相關(guān)技巧,需要的朋友可以參考下
    2016-04-04
  • Android9 清除最近進(jìn)程列表實現(xiàn)方法

    Android9 清除最近進(jìn)程列表實現(xiàn)方法

    這篇文章主要為大家介紹了Android9 清除最近進(jìn)程列表實現(xiàn)方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-06-06
  • Android 工程內(nèi)嵌資源文件的兩種方法

    Android 工程內(nèi)嵌資源文件的兩種方法

    Android軟件一般處理大的資源通過sdcard比如在線下載資源到sdcard,而apk中內(nèi)嵌資源或二進(jìn)制文件時一般使用下面的兩種方法:
    2013-01-01
  • flutter InheritedWidget使用方法總結(jié)

    flutter InheritedWidget使用方法總結(jié)

    這篇文章主要為大家介紹了flutter InheritedWidget使用方法總結(jié)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-11-11
  • Android入門之動態(tài)BroadCast的使用教程

    Android入門之動態(tài)BroadCast的使用教程

    系統(tǒng)自己在很多時候都會發(fā)送廣播,比如電量低或者充足,剛啟動完,插入耳機(jī),你有一條新的微信消息,這種都是使用BroadCast機(jī)制去實現(xiàn)的。BroadCast分為靜態(tài)和動態(tài)BroadCast兩種,本文就來聊聊動態(tài)BroadCast的使用,需要的可以參考一下
    2022-12-12

最新評論