標籤:android style blog class code c
對於一個"我們從來不生產代碼,我們只是大自然代碼的搬運工"的碼農來說。對android的觸控機制一直是模稜兩可的狀態,特別是當要求一些自訂的控制項和androide的內建控制項(比如ViewPager,ListView,ScrollView)高度嵌套在一起使用時。
花了點時間梳理了下,做個筆記。對於一個觸控的事件從使用者輸入到傳遞到Actigvity到最外層的Viewgroup在到子View,中間過程還可能穿插多個Viewgroup,android在ViewGroup提供了3個方法來控制流程程的分發,攔截,和執行。他們按照執行的順序分別是dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()。
一.主要方法介紹
打個比喻,所有的事件(可以看做美食),要消化沒事,首先先要判斷桌前的食物你是否喜歡(dispatchTouchEvent相當選擇美食的手)。
dispatchTouchEvent(MotionEvent e):根據入參MotionEvent來決定經過onInterceptTouchEvent和標記變數disallowIntercept(稍候會描述)的值,來判斷將事件傳遞到給自己的onTouchEvent()方法還是傳遞給子View或者者子ViewGroup來再次進行分發,還是直接自己處理掉(調用自身的onTouchEvent方法)。
當返回False的時候,表明這次事件不需要分發,把事件扼殺在搖籃裡了。:)
當回回True的時候,表明消化了改事件,進入下一步,可以看流程圖(用吃貨的話來說,就是我沒吃飽,再來一打小笼包也沒問題)。
當食物進入嘴巴的時候,對於吃不了辣的人來說。假如吃的食物非常非常的辣的話,在進入胃之前舌頭是可以判斷美食是否好吃選擇吞到自己的肚子裡的。onInterceptTouchEvent在這裡就相當與人的舌頭,他是第二層判斷食物是否要下咽的器官。
boolean onInterceptTouchEvent(MotionEvent e):決定是否攔截下事件,以便給當前View的TouchEvent來處理。
當改方法返回True的時候,事件會停止向下分發,而調用當前View的onTouchEvent方法來處理,(對於不愛吃香菜的人來說,一晚拉麵中要是不小心參雜了一絲香菜,必然是吐之的)。
當返回為False的時候的時候,事件會繼續分發到子View進行消化(碰到美食當然要讓他到自己的胃裡好好犒勞犒勞自己咯)。
onTouchEvent方法才是真正消化食物的胃,事件真正的處理是在這個地方。
boolean onTouchEvent(MotionEvent e) :真正的訊息處理的函數,他的返回值,直接影響到事件下次是否回傳給父類View的onTouch方法。
當返回值為True時,表明當前View已經消化了該事件,不需要父View繼續再消化了(不調用父類onTouchEvent)(吃了個味道不錯的肉包,還沒吃飽,可以接著吃。
當返回值為False時,表明當前View不能消化該事件,需要父View再消化消化(調用父類onTouchEvent) (人們碰到食物不好吃的時候,有時候也會引起反酸的麼,告訴人體,這可不好吃。
以上的的過程都是從 手—>嘴—>胃的循序漸進的過程。手和嘴都是可以調用onInterceptTouchEvent方法來決定是否傳遞下去。
不過android的觸控流程還有一個重要的方法:
void requestDisallowInterceptTouchEvent(Boolean disallowIntercept):這個方法的入參一個bool變數,用來表示是否需要調用onInterceptTouchEvent來判斷是否攔截.
該標記如果為True,就如它的字面意思一樣——不允許調用onInterceptTouchEvent(),結果就是,所有的父類方法都不會進行攔截,而把事件傳遞給子View. 該方法屬於ViewGroup ,並且是個遞迴方法,也就是說一旦調用後,所有父類的disallowIntercept都會設定成True。即當前View的所有父類View,都不會調用自身的onInterceptTouchEvent()進行攔截。
requestDisallowInterceptTouchEvent方法的代碼
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } //設定當前View的disallowIntercept if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // 遞迴調用,設定父類的disallowIntercept if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }
二.總結
下基本的規則是:
1. 如果當前的ViewGroup的onInterceptTouchEvent()在接收到down事件處理完成之後return false,那麼後續的move, up等事件將繼續會先傳遞給該ViewGroup,之後才和down事件一樣傳遞給最終的目標view的onTouchEvent()處理。
如果當前ViewGroup的onInterceptTouchEvent()在接收到down事件處理完成之後return true,那麼後續的move, up等事件將不再傳遞給onInterceptTouchEvent(),而是和down事件一樣傳遞給該ViewGroup的onTouchEvent()處理,注意,目標view將接收不到任何事件。
2. 如果當前ViewGroup的onTouchEvent()返回了false,那麼該事件將被傳遞至其上一層次的view的onTouchEvent()處理。
如果當前ViewGroup 的onTouchEvent()返回了true,那麼後續事件將可以繼續傳遞給該view的onTouchEvent()處理。
3。 如果當前view.dispatchTouchEvent()返回true,父view中將不再處理該訊息,但前提是該訊息沒有被父view截取,在整個touch訊息處理過程中,若處理函數返回true,我們稱之為消費了該touch事件,並且後面的父view將不再處理該訊息。
注意一點:在整個touch事件程序中,從action_down到action_up,若父ViewGroup的函數onInterceptTouchEvent一旦返回true,訊息將不再派發給子view,細分可為兩種情況,若是在action_down時onInterceptTouchEvent返回true,不會派發任何訊息給子view,並且後面onInterceptTouchEvent函數將不再會被執行,若是action_down時onInterceptTouchEvent返回false ,而後面touch過程中onInterceptTouchEvent==true,父viewGroup會把action_cancel派發給子view,也之後不再派發訊息給子view,並且onInterceptTouchEvent函數後面將不再被執行。
很多情況下,在自訂的ViewGroup中繼承了onInterceptTouchEvent簡單粗暴的返回true,假如自訂的ViewGroup中包含Button,就會發現Button的Onclick方法執行不了。應為要形成一個必須要先要的道一個ACTION_DOWN的事件,onInterceptTouchEvent把Down事件也截取的話,內部的Button自然就不會回掉Onclick了
三.流程圖總結
如果仔細查看ViewGroup的onTouchEvent和InterceptTouchEvent這兩個類的話,onInterceptTouchEvent的整個過程是沒有調用過onTouchEvent的,反之也是,那麼onInterceptTouchEvent是如何攔截事件的流程的呢?
答案在dispatchTouchEvent中,dispatchTouchEvent控制了整個流程是被攔截自己消化,還是傳遞給子View繼續消化,是他調用了onInterceptTouchEvent和onTouchEvent進行判斷
來看下整個流程用流程圖表示如下:
四.跟蹤源碼,知其所以然
現在看看最最重要的ViewGroup.DispatchTouchEvent()源碼 ,探究下剛才的3條規則代碼實現的原理
DispatchTouchEvent的整個流程會先判斷disallowIntercept的值,然後再調用自身的onInterceptTouchEvent來判斷是否攔截,假如攔截的話,就調用自身的onTouchEvent,假如未攔截,就調用子View的DispatchTouchEvent,假如子View是個ViewGroup的話,就又會遞迴調用自己的DispatchTouchEvent方法了。假如是子View是個單純的View的話,就會調用onTouchEvent了,因為View不能再包含View了,也自然就不需要攔截了,當然也沒有onInterceptTouchEvent方法的必要了
@Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // Check for interception. final boolean intercepted;//攔截的標記變數 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { //調用自身的 onInterceptTochEvent,判斷是否需要攔截 intercepted = onInterceptTochEvent(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; } //假如沒攔截 if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { if (newTouchTarget == null && childrenCount != 0) { 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. final View[] children = mChildren; final boolean customOrder = isChildrenDrawingOrderEnabled(); //遍曆所有的子View,並且調用他們的事件分發方法dispatchTouchEvent() for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = children[childIndex]; if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } newTouchTarget = getTouchTarget(child); //newTouchTarget表示事件傳遞的View目標,當不為空白的時候,直接跳出迴圈 if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); //遞迴調用子View分發事件方法, if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { mLastTouchDownTime = ev.getDownTime(); mLastTouchDownIndex = childIndex; mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //設定分發目標newTouchTarget為當前View newTouchTarget = addTouchTarget(child, idBitsToAssign); //標記子View的分髮結果,為True的話,下面的代碼是不會調用當前View的onTouch方法的,也就是規則1產生的原因 alreadyDispatchedToNewTouchTarget = true; break; } } } } } if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. 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. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; //如果剛才alreadyDispatchedToNewTouchTarget設為True的話,就不執行下面的dispatchTransformedTouchEvent //alreadyDispatchedToNewTouchTarget是由子View的onTouch傳回值決定的, if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //執行自身的Touch事件, if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } } 再來看看View的dispatchTouchEvent方法,十分簡單,基本上就是調用onTouch()方法,並返回結果。
public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); }//... if (onTouchEvent(event)) {//View的dispatchTouchEvent十分簡單,就是調用下自己onTouch事件,並且返回結果。這個結果直接影響以後的事件是否///繼續傳遞 return true; } }//... return false;}
所以,搞清楚3個主要的方法,並且理順每個方法的返回值,清楚的知道返回值對其他方法的影響,是搞清楚觸控模型的關鍵。就可以做到,需求再如何變,也可以把app的互動做好。