Android O: 觸控事件傳遞流程原始碼分析(上)
前面的部落格中,我們通過例子分析了一下Android中事件傳遞的流程,
詳細內容可以參考:Android觸控事件傳遞機制簡要分析
貫穿整個Android的觸控事件分發的流程,基本可以抽象成以下的虛擬碼:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean handle = false;
if(onInterceptTouchEvent(ev)){
handle = onTouchEvent(ev);
}else{
handle = child.dispatchTouchEvent(ev);
}
return handle;
}
如果一個事件傳遞到了ViewGroup處,首先會判斷當前ViewGroup是否要攔截事件,
即呼叫onInterceptTouchEvent()方法。
如果返回true,則表示ViewGroup攔截事件,
那麼ViewGroup就會呼叫自身的onTouchEvent來處理事件;
如果返回false,表示ViewGroup不攔截事件,
此時事件會分發到它的子View處,即呼叫子View的dispatchTouchEvent方法。
如此反覆直到事件被消耗掉。
本篇部落格,我們以Android O的程式碼為例,看看對應流程的原始碼。
考慮到事件分發的流程較為繁瑣,本篇部落格主要針對Activity和ViewGroup部分。
一、Activity部分
系統有一個執行緒在迴圈收集螢幕硬體資訊。
當用戶觸控式螢幕幕時,該執行緒會把從硬體裝置收集到的資訊,
封裝成一個MotionEvent物件,然後把該物件存放到一個訊息佇列中。
與此對應,系統的另一個執行緒迴圈的讀取訊息佇列中的MotionEvent,然後交給WMS去派發。
WMS把該事件派發給當前處於Active狀態的Activity,即處於活動棧最頂端的Activity。
我們就從Activity的dispatchTouchEvent入手,看看整個事件分發的流程:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//空實現,其用途註釋已經說的比較清楚了
//若需要了解使用者點選介面,可以自行實現該介面
//Implement this method if you wish to know that the user has
//interacted with the device in some way while your activity is running.
onUserInteraction();
}
//首先進行事件分發,事件被消費掉就會返回true
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果事件沒有沒消費掉,那麼就會呼叫Activity的onTouchEvent
return onTouchEvent(ev);
}
從上面的程式碼容易看出,Activity收到觸控事件後首先會進行分發;
如果事件在分發的過程中沒被消費掉,最終會被Activity的onTouchEvent處理。
在繼續分析之前,我們先看看Activity的getWindow函式:
public Window getWindow() {
//返回Activity持有的mWindow物件
return mWindow;
}
mWindow物件是Activity顯示在介面上時建立的,如下所示:
final void attach(.....) {
........
//實際上是一個PhoneWindow物件
mWindow = new PhoneWindow(this, window, activityConfigCallback);
........
}
由上面的程式碼易知,Activity分發的事件實際上交給了PhoneWindow。
對應分發事件的函式如下:
public boolean superDispatchTouchEvent(MotionEvent event) {
//mDecor的型別為DecorView,在PhoneWindow初始化時得到
return mDecor.superDispatchTouchEvent(event);
}
DecorView繼承FrameLayout,後者繼承ViewGroup,
於是上述程式碼最終會將觸控事件遞交給ViewGroup處理。
二、ViewGroup部分
接下來我們跟進ViewGroup的dispatchTouchEvent函式。
2.1 處理Accessibility Focus事件
public boolean dispatchTouchEvent(MotionEvent ev) {
.......
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
.......
ViewGroup收到觸控事件後,首先需要處理特殊情況,
即攜帶FLAG_TARGET_ACCESSIBILITY_FOCUS的MotionEvent。
該標誌位的含義可以參考註釋:
/**
* 1、 Private flag indicating that this event was synthesized by the system and
* should be delivered to the accessibility focused view first.
*
* 2、 When being dispatched such an event is not handled by predecessors of the accessibility
* focused view and after the event reaches that view the flag is cleared and
* normal event dispatch is performed.
* .......
* /
從上述註釋可以看出,當MotionEvent具有該Flag時,
若點選區域存在accessibility focused view(或ViewGroup),那麼事件會優先被遞交給這種View處理。
即其父View或ViewGroup,無法消費該MotionEvent(從程式碼來看,ViewGroup仍可以攔截)。
當accessibility focused view收到該MotionEvent後,就會呼叫上面的程式碼,
清除掉FLAG_TARGET_ACCESSIBILITY_FOCUS標籤。
此後,MotionEvent就會按照普通的邏輯被分發和消費。
從上面的程式碼也可以看出,當MotionEvent攜帶Flag時,
會利用isAccessibilityFocusedViewOrHost函式,判斷當前View是否有能力處理accessibility focused motion event。
如果滿足條件,將會呼叫setTargetAccessibilityFocus函式,取消掉FLAG_TARGET_ACCESSIBILITY_FOCUS。
我們看看isAccessibilityFocusedViewOrHost函式:
//實際上定義在ViewGroup的父類View中
boolean isAccessibilityFocusedViewOrHost() {
//判斷View是否具有PFLAG2_ACCESSIBILITY_FOCUSED
//或者利用view root判斷當前View是否具有ACCESSIBILITY_FOCUSED能力
return isAccessibilityFocused() || (getViewRootImpl() != null && getViewRootImpl()
.getAccessibilityFocusedHost() == this);
}
實際上當一個View呼叫requestAccessibilityFocus後,就會具有accessibility focus:
public boolean requestAccessibilityFocus() {
//ViewManager enable 且 View可視時,就可以申請accessibility focus
AccessibilityManager manager = AccessibilityManager.getInstance(mContext);
if (!manager.isEnabled() || !manager.isTouchExplorationEnabled()) {
return false;
}
if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
if ((mPrivateFlags2 & PFLAG2_ACCESSIBILITY_FOCUSED) == 0) {
//置標誌位
mPrivateFlags2 |= PFLAG2_ACCESSIBILITY_FOCUSED;
//在ViewRoot中記錄
ViewRootImpl viewRootImpl = getViewRootImpl();
if (viewRootImpl != null) {
viewRootImpl.setAccessibilityFocus(this, null);
}
//重新整理
invalidate();
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
return true;
}
return false;
}
2.2 過濾事件
處理完特殊事件後, 需要進行安全性檢測,如下程式碼所示:
//用於記錄事件最終是否被處理
boolean handled = false;
//滿足條件的事件才能被繼續處理
if (onFilterTouchEventForSecurity(ev)) {
.....
onFilterTouchEventForSecurity實際上也定義於ViewGroup的父類View中:
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//判斷View是否具有FILTER_TOUCHES_WHEN_OBSCURED
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
//判斷事件是否具有FLAG_WINDOW_IS_OBSCURED
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
//該標誌位的含義表示:當View所在的Window被遮擋且事件攜帶對應標誌時,應該過濾收到的觸控事件
return false;
}
return true;
}
為了理解增加過濾判斷的原因,我們需要看看FLAG_WINDOW_IS_OBSCURED的註釋:
/**
* 1、window被覆蓋時,收到其上window的觸控事件時,就會新增該標誌位
* This flag indicates that the window that received this motion event is partly
* or wholly obscured by another visible window above it. This flag is set to true
* even if the event did not directly pass through the obscured area.
*
* 2、新增標誌位的原因: 防止惡意應用覆蓋在正常應用之上,擷取使用者資訊或誤導使用者
* 例如:需要輸入密碼時,惡意應用新增透明介面,就可能擷取使用者輸入的資訊
* A security sensitive application can check this flag to identify situations in which
* a malicious application may have covered up part of its content for the purpose
* of misleading the user or hijacking touches. An appropriate response might be
* to drop the suspect touches or to take additional precautions to confirm the user's
* actual intent.
*/
2.3 攔截事件
如果觸控事件沒有被過濾掉,那麼就可以開始正常處理了,對應原始碼如下:
............
//首先得到觸控事件的型別
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
//Handle an initial down.
//所有的觸控事件都以ACTION_DOWN開始
//因此,當收到ACTION_DOWN事件時,表明新的觸控事件發生了
//此時會清除舊有的狀態
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
//一個MotionEvent從ACTION_DOWN開始,到ACTION_UP結束
//其間會有多個型別的事件傳遞到View
//這些事件傳遞的物件,會用一個連結串列TouchTargets記錄起來
//當一個ACTION_DOWN發生時,該函式中會呼叫clearTouchTargets,
//清除舊有的記錄
resetTouchState();
}
// Check for interception.
final boolean intercepted;
//如果收到新的ACTION_DOWN事件
//或者其它型別的事件,但之前已經找到傳遞View
//則進入常規攔截流程
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//mFirstTouchTarget指向前一個事件傳遞的View
//不為null, 說明之前已經有事件未被攔截
//首先需要判斷ViewGroup是否允許攔截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//允許攔截時呼叫onInterceptTouchEvent函式
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.
// 進入這個分支,以為收到非ACTION_DOWN事件,且mFirstTouchTarget = null
// 那麼說明這個MotionEvent的ACTION_DOWN事件就被攔截了
// 那麼之後的UP、MOVE等操作,都會被攔截
intercepted = true;
}
//If intercepted, start normal event dispatch.
//Also if there is already a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
..........
除了特殊的MotionEvent以外,預設情況下,
ViewGroup的onInterceptTouchEvent將返回false,
即不攔截MotionEvent。
2.4 查詢處理ACTION_DOWN等事件的View
若觸控事件沒有被擷取,就需要查詢處理事件的View了。
// Check for cancelation.
// 首先判斷該事件是否需要被取消
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
//判斷是否可以將事件分發給多個子View
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
//用於記錄本次分發事件的目標
TouchTarget newTouchTarget = null;
//記錄是否成功分發
boolean alreadyDispatchedToNewTouchTarget = false;
//如果事件沒有被攔截或取消,就可以開始進行分發
if (!canceled && !intercepted) {
//與前面第一部分對應, 對於一個特殊的事件來說
//如果當前ViewGroup可以處理,在第一部分已經清除掉了flag
//若無法處理,則在這裡查詢是否存在可以處理的child view
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//開始處理ACTION_DOWN型別的事件
..................
從上面程式碼可以看出,當MotionEvent沒有被攔截或取消時,才會執行後續的流程。
否則,將直接進入下一部分(即2.5小節)。
//對於以下型別的MotionEvent,才需要重新查詢target View
//其中比較有代表性的就是ACTION_DOWN
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
//如果需要將事件分發給多個子View, 則得到actionIndex對應的pointerId
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
// 清除idBitsToAssign對應target的舊有狀態
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
//開始進行查詢
if (newTouchTarget == null && childrenCount != 0) {
//得到觸控事件對應的座標,根據座標才能得到可以處理該事件的child view
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
// 看函式名就應該能知道含義
// 此處按順序得到ViewGroup的child view
// 從變數名來看,得到的是先序遍歷的結果
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
//此處對應child view按特定順序繪製的情況
//需要參考getChildDrawingOrder和setChildrenDrawingOrderEnabled
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
//逆序開始查詢Child View
//這麼做的原因是:
//後新增的View作為子View,被繪製在父View的上層
//於是,按照先序遍歷時,後新增的子View肯定處於List的後端
//我們點選介面時,按照邏輯,肯定應該讓最上面的View優先進行處理
//結合上面的原因,就應該讓處於List末端的子View,優先處理MotionEvent
for (int i = childrenCount - 1; i >= 0; i--) {
//從preorderedList取出View
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//需要處理Accessibility Focus事件時,優先找出對應的View
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
//一旦找到, 就將觸控事件遞交給該View
//此處置為null, 表明不再尋找具有Accessibility Focus能力的View
//因此,只有第一個具有Accessiliblity focus能力的View有機會處理這種型別的事件
childWithAccessibilityFocus = null;
//此處,將i重置為childrenCount - 1
//意味著若具有Accessibility Focus能力的View, 無法處理該MotionEvent
//那麼將按照正常流程, 重新找出能夠處理該MotionEvent的View
i = childrenCount - 1;
}
//判斷View能否處理MotionEvent
//canViewReceivePointerEvents主要判斷View是否可見(可見或在播放動畫)
//isTransformedTouchPointInView主要判斷MotionEvent的座標是否落在View內
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
// 如果不能處理該View, 則跳過
//程式碼進入到這裡,意味著:
//沒有Accessibility Focus View
//或有Accessibility Focus View, 但無法處理該MotionEvent
//無論哪種情況,都去掉標誌,將MotionEvent當作普通事件處理
ev.setTargetAccessibilityFocus(false);
continue;
}
//判斷View是否已經接收過該事件(其它的型別)
//如果接收過該事件,那麼newTouchTarget != null
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
//找到了接收事件的View則break, 退出查詢過程
break;
}
//清除child view的PFLAG_CANCEL_NEXT_UP_EVENT
resetCancelNextUpFlag(child);
//dispatchTransformedTouchEvent的主要功能,是按需調整MotionEvent,
//然後遞交給child view的dispatchTouchEvent處理
//當child為null時,由ViewGroup自己處理
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//一旦事件被消耗掉,則更新相關的狀態
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//新的TouchTarget被記錄到整個連結串列中, 並作為新的mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
//處理沒有找到newTouchTarget的情況
//將idBitsToAssign付給前一次的TouchTargets
//這麼做的目的不是很清楚
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
............
2.5 其它
對於ACTION_DOWN等型別的事件來說,嘗試查詢負責處理的View後,
需要開始進行後續的收尾工作。
對於其它型別的事件而言,這裡是處理的起點。
//Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
// 分發MotionEvent失敗,沒有找到mFirstTouchTarget, 該事件將被遞交給ViewGroup處理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
// 進入到這個分支時,可能是ACTION_DOWN事件,也可能是其它型別的事件
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//輪詢連結串列,找到處理事件的target
while (target != null) {
final TouchTarget next = target.next;
//這個if針對的是已經處理的ACTION_DOWN事件, 將handled置為true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//針對未處理的ACTION_DOWN或其它型別事件
//判斷是否取消
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//分發給子檢視
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//如果某個檢視cancel, 就將其從連結串列中移除
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
//更新“指標”
predecessor = target;
target = next;
}
}
// 後續收尾工作,主要根據事件型別、處理的結果,更新一些狀態
...............
三、總結
至此,我們已經結合原始碼大致瞭解ViewGroup的事件分發邏輯。
這部分內容細節比較多,但整體邏輯比較清晰,大致如下圖所示:
ViewGroup有攔截事件的能力,同時也有分發事件給Child View的能力。
這導致ViewGroup的事件處理流程比較複雜。
相對而言,View處理觸控事件的流程較為簡單,我們在下一篇部落格中再來分析。