首頁 Java java教程 Android View 事件分送機制詳解

Android View 事件分送機制詳解

Jan 16, 2017 pm 04:33 PM

Android開發,觸控無所不在。對於一些 不咋看源碼的同學來說,多少對這塊都會有一些疑惑。 View事件的分發機制,不僅在做業務需求上會碰到這些問題,在一些面試筆試題中也常有人問,可謂是老生常談了。我以前也看過很多人寫的這方面的文章,不是說的太囉嗦就是太模糊,還有一些在細節上寫的也有爭議,故再次重新整理一下這塊內容,十分鐘讓你搞懂View事件的分發機制。

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

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

三個關鍵方法

public boolean dispatchTouchEvent(MotionEvent ev)

View/ViewGroup處理事件分發的發起者,View/ViewGroup接收到觸控事件最先調起的判斷這個方法,然後在這個方法中是否處理攔截或是將事件分發給子容器

public boolean onInterceptTouchEvent(MotionEvent ev)

ViewGroup專用,透過此方法可以達到控制事件的分發方向,一般可以在該方法中判斷將事件給ViewGroup獨吞或是它繼續傳遞給子容器,是處理事件衝突的最佳地點

public boolean onTouchEvent(MotionEvent event)

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

核心問題

時間分發機制的難點在哪,我覺得難的地方以下幾點:三個方法呼叫規則,決定處理事件的物件以及事件衝突的解決方法。

事件傳遞規則

一般一次點擊會有一系列的MotionEvent,可以簡單分為:down->move->….->move->up,當一次event分發到ViewGroup時,上述三個方法之間的ViewGroup中呼叫順序可以用一段簡單程式碼表示

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;
}
登入後複製

回傳結果true表示事件被處理了,回傳false表示沒有處理。上面的程式碼簡單易懂,看起來也很簡單,一句話就能概括,ViewGroup收到事件後呼叫dispatch,在dispatch中先檢查是否要攔截,若攔截則ViewGroup吃掉事件,否則交給有處理能力的子容器處理。

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

onInterceptTouchEvent呼叫條件

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暫時不攔截事件,事件有機會傳給子View處理,返回true表示父容器直接攔截了該系列事件,後續不會再傳遞給子View了。子View想取得事件只能讓該值為false

onInterceptTouchEvent呼叫回傳false(回傳false才能傳遞給子View,對應到上面偽代碼的else中的內容,叫事件傳遞到子容器需要滿足的內容較好理解一些)需要滿足兩個條件中的任意一個就有可能觸發(當然只是有可能):

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

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

disallowIntercept這個變數很有意思,它的值主要受FLAG_DISALLOW_INTERCEPT這個標記影響,這個值可以被ViewGroup的子View設置,ViewGroup的子View如果調用了requestDisallowInterceptTouch)這個方法,會改變FLAG_DISALL ,這種情況會跳過intercept,導致攔截失效。

但这事还没了,FLAG_DISALLOW_INTERCEPT这个标记有一个重置的机制,查看ViewGroup源码可以看到,在处理MotionEvent.ACTION_DOWN的时候会重置这个标记导致disallowIntercept失效,是不是丧心病狂,上面的一段这么简单的代码有这么多幺蛾子,这里还能得到一个结论,ACTION_DOWN的时候肯定可以执行onInterceptTouchEvent的。

所以拦截的intercepted很重要,能影响到底是让ViewGroup还子View处理这个事件。

上面的两个有可能触发拦截的条件说完了,那么当两个条件都不满足的话就不会再调用拦截了(拦截很重要,一般ViewGroup都返回false这样能把事件传递给子View,如果在ACTION_DOWN时不能走到OnInterceptTouchEvent并返回false告诉ViewGroup不要拦截,则事件再也不能传给子View了,所以拦截一般都是要走到的,而且一般都是返回false这样能让子View有机会处理),这种情况一般都是在ACTION_DOWN处理完之后没有子View当接盘侠消费ACTION_DOWN以及后续事件,从上面的伪代码可以看出来,这时候ViewGroup自己就很被动了,需要自己来调用onTouchEvent来处理,这锅就自己背了。

再继续说一下mFirstTouchTarget和intercepted是怎么影响事件方向的。看源码:

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不为空才能让后续事件传递给子View,否则根据上上面说的代码后续事件只能给父容器处理了。

mFirstTouchTarget就是我们后续事件传递的对象,很容易理解,如果在ACTION_DOWN中没有确定这个对象,则后续事件不知道传递给谁自然就交给父容器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;
    }
 
}
登入後複製


看到没,只要参数里传的child为空,则ViewGroup调用super.dispatchTouchEvent(event),super是谁,ViewGroup继承自View,当然是View咯,View的dispatch调用的谁?当然是自己的onTouchEvent(后面会说),所以这个最后还是调用了ViewGroup自己的onTouchEvent。

那么当child!=null的时候呢,调用的是child的dispatchTouchEvent(event),如果child可能是View也可能是ViewGroup,如果是ViewGroup则继续按照上面的伪代码执行事件分发,如果也是View则调用自己的onTouchEvent。

所以,说到底事件到底给谁处理,还是和传进来的child有关,那这个方法在哪里调用的呢,继续看:

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能影响事件分发的方向的原因。就这样,整个伪代码的流程是不是很清楚了。

这里需要多说两句,在上上面代码流程中,intercepted决定了这个事件会不会调用ViewGroup的onTouchEvent,当intercepted为true则后续流程会调用ViewGroup的onTouchEvent,仔细看上面的代码能发现,只有两种情况为ture:一是调用了InterceptTouchEvent把事件拦截下来,另一个就是没有一个子View能够消费ActionDown。只有这两种情况父容器ViewGroup才会自己处理
那么问题来了,思考一个问题:如果子View处理了ACTION_DOWN但后续事件都返回false,这些没有被处理的事件最后传给谁处理了?各位思考之,后面再说这个问题。

孩子是谁的

继续来扩展我们的伪代码,拦截条件判断完之后,决定把事件继续传递给子View的时候,会调用childView.dispatchTouchEvent(ev),问题来了,child是哪来的,继续看源码、

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

ViewGroup通过判断所有的子View是否可见是否在播放动画和是否在点击范围内来决定它是否能够有资格接受事件。只有满足条件的child才能够调用dispatch。

再看伪代码,最后dispatch返回ViewGroup的isConsumed,若isConsume == true,说明ViewGroup处理了这个点击事件(ViewGroup自身或者子View处理的),并且这个系列的点击事件会继续传到这个ViewGroup来处理,若isConsume == false(ACTION_DOWN时),ViewGroup没办法处理这个点击事件,那么这个系类的点击事件就和该ViewGroup无缘了。会把这个事件上抛给自己的父容器或者Activity处理。

伪代码说完了,ViewGroup的事件传递规则也就差不多说完了,这么看是不是很简单了。View相对于ViewGroup来说就更简单了,没有拦截方法,dispatch基本上是直接调用了自身的onTouchEvent,处理起来一点难度都木有呀。

一些没说到但也很重要的点

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

順帶補充幾點:

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

事件衝突解決可以按照上面的原理在幾個point中處理。最容易想到的處理的時機是在onInterceptTouchEvent裡,例如當一個垂直方向滑動的ViewGroup裡嵌套一個橫向滑動的ViewGroup,可以在這裡的ACTION_MOVE裡來判斷後續事件應該傳遞給誰處理,當然,也可以根據上面所說的標記位FLAG_DISALLOW_INTERCEPT配合子View的dispatchTouchEvent來控制事件的流向,這都是比較容易想到的,不過看過別的大神,透過分享MotionEvent的方法來控制事件的流向,即在父容器中保存MotionEvent並在適當的時機傳入子View自訂的事件處理方法來分享事件,也是可行的。

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

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

理解事件分發的機制只要明白上面的原理基本就夠用了,github上很多牛逼的大神寫的各種炫酷的自定義控件的事分發根據這些也能夠看明白,當然還有很多擴充的東西和更深入的內容由於篇幅的關係在這裡就不囉嗦了,更重要的還是去看源碼吧。
最後送各位一句經典:紙上得來終覺淺,絕知此事要躬行!

以上就是Android View事件分發機制的資料整理,後續繼續補充相關資料,謝謝大家對本站的支持!

更多Android View 事件分發機制詳解相關文章請關注PHP中文網!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1653
14
CakePHP 教程
1413
52
Laravel 教程
1306
25
PHP教程
1251
29
C# 教程
1224
24
公司安全軟件導致應用無法運行?如何排查和解決? 公司安全軟件導致應用無法運行?如何排查和解決? Apr 19, 2025 pm 04:51 PM

公司安全軟件導致部分應用無法正常運行的排查與解決方法許多公司為了保障內部網絡安全,會部署安全軟件。 ...

如何將姓名轉換為數字以實現排序並保持群組中的一致性? 如何將姓名轉換為數字以實現排序並保持群組中的一致性? Apr 19, 2025 pm 11:30 PM

將姓名轉換為數字以實現排序的解決方案在許多應用場景中,用戶可能需要在群組中進行排序,尤其是在一個用...

如何優雅地獲取實體類變量名構建數據庫查詢條件? 如何優雅地獲取實體類變量名構建數據庫查詢條件? Apr 19, 2025 pm 11:42 PM

在使用MyBatis-Plus或其他ORM框架進行數據庫操作時,經常需要根據實體類的屬性名構造查詢條件。如果每次都手動...

如何使用MapStruct簡化系統對接中的字段映射問題? 如何使用MapStruct簡化系統對接中的字段映射問題? Apr 19, 2025 pm 06:21 PM

系統對接中的字段映射處理在進行系統對接時,常常會遇到一個棘手的問題:如何將A系統的接口字段有效地映�...

Java對像如何安全地轉換為數組? Java對像如何安全地轉換為數組? Apr 19, 2025 pm 11:33 PM

Java對象與數組的轉換:深入探討強制類型轉換的風險與正確方法很多Java初學者會遇到將一個對象轉換成數組的�...

IntelliJ IDEA是如何在不輸出日誌的情況下識別Spring Boot項目的端口號的? IntelliJ IDEA是如何在不輸出日誌的情況下識別Spring Boot項目的端口號的? Apr 19, 2025 pm 11:45 PM

在使用IntelliJIDEAUltimate版本啟動Spring...

電商平台SKU和SPU數據庫設計:如何兼顧用戶自定義屬性和無屬性商品? 電商平台SKU和SPU數據庫設計:如何兼顧用戶自定義屬性和無屬性商品? Apr 19, 2025 pm 11:27 PM

電商平台SKU和SPU表設計詳解本文將探討電商平台中SKU和SPU的數據庫設計問題,特別是如何處理用戶自定義銷售屬...

如何利用Redis緩存方案高效實現產品排行榜列表的需求? 如何利用Redis緩存方案高效實現產品排行榜列表的需求? Apr 19, 2025 pm 11:36 PM

Redis緩存方案如何實現產品排行榜列表的需求?在開發過程中,我們常常需要處理排行榜的需求,例如展示一個�...

See all articles