深入理解 View 的事件傳遞機制,深入理解view
引言:現在 GitHub 上酷炫的 Android 控制項越來越多,一方面我們可以讓 App 各美觀,另一方面我們這些開發人員也可以從中學習到各種知識。寫下這篇博文主要是記錄研究自訂控制項源碼過程中接觸到的知識盲區,協助自己鞏固知識的同時,也和大家交流學習,一起進步。
Demo源碼
廢話不多說,進入正題:
一、概述 View 事件傳遞機制
使用者通過點擊、滑動螢幕與 App 產生互動是移動互連網時代的互動基礎,那麼在 Android 中,使用者的點擊、滑動是怎麼和 Android 系統產生互動的呢?
在 Android 中,我們所說的點擊、滑動等事件,都被視為 MotionEvent ,而在 MotionEvent 中,我們的操作行為被歸類為以下常量:
- ACTION_DOWN
- ACTION_UP
- ACTION_MOVE
- ACTION_POINTER_DOWN
- ACTION_POINTER_UP
- ACTION_CANCEL
除此以外,為了讓系統更好地管理和操作這些事件,MotionEvent 還需要記錄事件的發生時間,判斷事件是單點觸控/多點觸控以及事件的發生時間。可能有人會問了,就這麼點常量夠我們判斷我們的手勢嗎?莫慌,Google 對事件可是有著明確的區分標準呢:一次觸控操作,起於 ACTION_DOWN 終於 ACTION_UP。簡單的觸控操作,如:點擊、滑動等,很輕鬆就能通過這些常量判斷出來;而複雜的手勢,則需要根據你手指的滑動軌跡不斷地對事件座標進行分析了。
註:為了簡化理解,後文中我將把所有和點擊、滑動等等有關的事件歸類為點擊事件
二、View 事件的傳遞流程
之後的解析都會結合源碼進行,沒基礎的小夥伴要認真跟上哦
在 Android 中,當一個點擊事件被傳入,首先執行 Actvity 的 dispatchTouchEvent() 方法接收事件,Activity 接收到事件之後交由根布局(即與 Activity 相關聯的 Window 中的布局,一般是 ViewGroup,如:常見的 LinearLayout、RelativeLayout等)進行分發,若根布局不需要處理該事件,除非某一個布局在傳遞過程中通過 onInterceptTouchEvent() 方法將事件攔截,並“消費”該事件(消費的概念將在下面通過源碼解釋),否則事件將一直向下傳遞給子布局。若事件傳遞至最底層子布局中仍未被處理,則會反過來一直向上傳遞,此時每一級父布局都能處理該事件。若事件回傳至根布局仍未被處理,則由 Activity 的 onTouchEvent() 方法終止事件。
註:OnTouchListener 處理事件的優先順序高於 onTouchEvent()
這樣一大段的敘述看下來,估計很多小夥伴都暈啦,其實俺也很暈噠~為了大家更好地理解,我先用一個 Demo 為大家介紹這個概念,再解析源碼:
Demo 代碼非常簡單,就是自訂 Button 和 LinearLayout,在執行相關的方法時輸出Log,Activity 裡執行相關方法也輸出 Log。點擊流程
很好地闡述了我們剛剛講解的 View 事件傳遞流程,下面來看源碼,更深一步地瞭解其中的機制:
首先,在 Activity 的 dispatchTouchEvent() 方法中對事件進行判斷(兩個判斷語句不用管),然後進入 onTouchEvent() 方法
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
在 onTouchEvent() 方法裡面會有一個判斷,shouldCloseOnTouch() 就是用於判斷事件是否從尾部回傳回來,true 代表事件應該不應該向下傳遞,而 false 代表事件未被消費,應該向下傳遞,交給子布局處理。
所以我們剛剛一直提到的“消費”的概念就是這個意思,事件在 View 鏈上傳遞一個來回,只要被處理了,並且處理它的 View 返回了 false,我們就認為事件被消費,如果返回 true ,我們則認為事件尚未被消費,仍需要向下傳遞,讓對應的 View 處理它。
public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }
現在 MyLinearLayout 通過 dispatchTouchEvent() 方法接收到事件,我們在 Demo 中只做了點擊, dispatchTouchEvent() 方法有涉及 move 和 up,所以我只抽取和 down 有關的那部分來講解
// Check for interception. 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 設為 true,使得後面的傳回值也為 true ,進而讓事件被當前 View 消費;不攔截的話,則將事件繼續向下傳遞。
後面的 MyButton 同理。
網上看到幾張圖把整個流程展現地很好: