上一篇咱們寫過View的事件分發機制,若是你對這還不瞭解的能夠看這一篇文章:android
https://my.oschina.net/quguangle/blog/793903ide
那麼今天咱們將繼續上次未完成的話題,從源碼的角度分析ViewGroup的事件分發。首先咱們來探討一下,什麼是ViewGroup?它和普通的View有什麼區別?源碼分析
顧名思義,ViewGroup就是一組View的集合,它包含不少的子View和子VewGroup,是Android中全部佈局的父類或間接父類,像LinearLayout、RelativeLayout等都是繼承自ViewGroup的。但ViewGroup實際上也是一個View,只不過比起View,它多了能夠包含子View和定義佈局參數的功能。ViewGroup繼承結構示意圖以下所示:佈局
能夠看到,咱們平時項目裏常常用到的各類佈局,全都屬於ViewGroup的子類。this
下面直接上案例:spa
package qu.com.handlerthread; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; /** * Created by quguangle on 2016/11/25. */ public class MyLinearLayout extends LinearLayout{ private static final String TAG = MyLinearLayout.class.getSimpleName(); public MyLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "dispatchTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "dispatchTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "dispatchTouchEvent ACTION_UP"); break; default: break; } return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "onTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "onTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "onTouchEvent ACTION_UP"); break; default: break; } return super.onTouchEvent(event); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "onInterceptTouchEvent ACTION_UP"); break; default: break; } return super.onInterceptTouchEvent(ev); } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { Log.e(TAG, "requestDisallowInterceptTouchEvent "); super.requestDisallowInterceptTouchEvent(disallowIntercept); } }
代碼依然的仍是那麼的簡單,重寫一些相關的方法。.net
而後看咱們的佈局文件:日誌
<?xml version="1.0" encoding="utf-8"?> <qu.com.handlerthread.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <qu.com.handlerthread.MyButton android:id="@+id/btnTest" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Button" android:onClick="btnTest"/> </qu.com.handlerthread.MyLinearLayout>
Activitycode
public class MainActivity extends AppCompatActivity { private static final String TAG = "MyButton"; private Button btnTest; private LinearLayout MyLinearLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnTest = (Button) findViewById(R.id.btnTest); btnTest.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { int action = motionEvent.getAction(); switch (action){ case MotionEvent.ACTION_DOWN: Log.e(TAG,"onTouch----ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG,"onTouch----ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG,"onTouch----ACTION_UP"); break; default: break; } return true; } }); btnTest.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Log.e(TAG,"onClick----"); } }); } }
佈局文件也很簡單,自定義MyLinearLayout 中放了一個以前用過的自定義MyButton,而後運行項目,我在點擊Button時任然Move下,否則不會出現ACTION_MOVE,看打印Log日誌:orm
從打印的日誌來看,大致上事件的流程爲:MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent
咱們如今換一種方式:Activity
public class MainActivity extends AppCompatActivity { private static final String TAG = "MyButton"; private Button btnTest; private LinearLayout MyLinearLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnTest = (Button) findViewById(R.id.btnTest); MyLinearLayout.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { int action = motionEvent.getAction(); switch (action){ case MotionEvent.ACTION_DOWN: Log.e(TAG,"onTouch----ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG,"onTouch----ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG,"onTouch----ACTION_UP"); break; default: break; } return false; } }); btnTest.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Log.e(TAG,"onClick----"); } }); } }
當咱們點擊Button時,打印狀況:
咱們的MyLinearLayout的onTouch方法並無執行。
而當咱們點擊空白區域時又執行了此方法,打印狀況:
Oh My Good!你能夠先理解成Button的onClick方法將事件消費掉了,所以事件不會再繼續向下傳遞。那就說明Android中的touch事件是先傳遞到View,再傳遞到ViewGroup的,這不跟咱們上面所說的相矛盾,難道真的是這樣嗎?
咱們從源碼中找真相:
ViewGroup - dispatchTouchEvent
2.1首先是ViewGroup的dispatchTouchEvent----ACTION_DOWN
@Override public boolean dispatchTouchEvent(MotionEvent ev) { if (!onFilterTouchEventForSecurity(ev)) { return false; } final int action = ev.getAction(); final float xf = ev.getX(); final float yf = ev.getY(); final float scrolledXFloat = xf + mScrollX; final float scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) { if (mMotionTarget != null) { // this is weird, we got a pen down, but we thought it was // already down! // XXX: We should probably send an ACTION_UP to the current // target. mMotionTarget = null; } // If we're disallowing intercept or if we're allowing and we didn't // intercept if (disallowIntercept || !onInterceptTouchEvent(ev)) { // reset this event's action (just to protect ourselves) ev.setAction(MotionEvent.ACTION_DOWN); // We know we want to dispatch the event down, find a child // who can handle it, start with the front-most child. final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; for (int i = count - 1; i >= 0; i--) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { child.getHitRect(frame); if (frame.contains(scrolledXInt, scrolledYInt)) { // offset the event to the view's coordinate system final float xc = scrolledXFloat - child.mLeft; final float yc = scrolledYFloat - child.mTop; ev.setLocation(xc, yc); child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; if (child.dispatchTouchEvent(ev)) { // Event handled, we have a target now. mMotionTarget = child; return true; } // The event didn't get handled, try the next view. // Don't reset the event's location, it's not // necessary here. } } } } } ....//other code omitted
因爲dispatchTouchEvent方法中代碼比較多,所以咱們首先分析ACTION_DOWN這部分。
1.進入ACTION_DOWN的處理。
2.將mMotionTarget置爲null。
3.進行判斷:if(disallowIntercept || !onInterceptTouchEvent(ev))根據判斷條件,咱們能夠將他分爲2中可能
特別提醒的是:disallowIntercept 能夠經過viewGroup.requestDisallowInterceptTouchEvent(boolean);進行設置,後面會詳細說;而onInterceptTouchEvent(ev)能夠進行復寫。
注意:若是說咱們在這裏使onInterceptTouchEvent返回值爲false,那麼它就不會進入IF,那麼咱們的button事件就會被屏蔽掉。
4.開始遍歷全部的子View
5.獲取當前觸摸點X,Y的座標,判斷是否落入在子View上,若是是就直接執行child.dispatchTouchEvent(ev)方法,意味這就進入到咱們以前講的View.dispatchTouchEvent(ev),不懂的能夠看我前面所講的,當child.dispatchTouchEvent(ev)返回值爲true,就將mMotionTarget=child,而後返回true.
到此ACTION_DOWN源碼結束了,可是並沒玩,還記得前面咱們的疑問嗎?
咱們已經知道,若是一個控件是可點擊的,那麼點擊該控件時,dispatchTouchEvent的返回值一定是true。由5可知當child.dispatchTouchEvent(ev)返回true,那麼就會直接進入到IF語句,而後返回true。後面的代碼就不會在執行了。
總結:
也就是說ViewGroup捕捉了DOWN事件,若是代碼中不作TOUCH事件攔截,則開始查找當前x,y是否在某個子View的區域內,若是在,則把事件分發下去。
2.2首先是ViewGroup的dispatchTouchEvent----ACTION_MOVE
@Override public boolean dispatchTouchEvent(MotionEvent ev) { final int action = ev.getAction(); final float xf = ev.getX(); final float yf = ev.getY(); final float scrolledXFloat = xf + mScrollX; final float scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //...ACTION_DOWN //...ACTIN_UP or ACTION_CANCEL // The event wasn't an ACTION_DOWN, dispatch it to our target if // we have one. final View target = mMotionTarget; // if have a target, see if we're allowed to and want to intercept its // events if (!disallowIntercept && onInterceptTouchEvent(ev)) { //.... } // finally offset the event to the target's coordinate system and // dispatch the event. final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc); return target.dispatchTouchEvent(ev); }
一樣咱們只看ACTION_MOVE代碼:
1.把ACTION_DOWN時賦值的mMotionTarget,付給target 。
2.if (!disallowIntercept && onInterceptTouchEvent(ev)) 當前容許攔截且攔截了,才進入IF體,固然了默認是不會攔截的~這裏執行了onInterceptTouchEvent(ev)。
3.把座標系統轉化爲子View的座標系統。
4.直接return target.dispatchTouchEvent(ev); 能夠看到,正常流程下,ACTION_MOVE在檢測完是否攔截之後,直接調用了子View.dispatchTouchEvent,事件分發下去;最後就是ACTION_UP了。
2.3首先是ViewGroup的dispatchTouchEvent----ACTION_UP
public boolean dispatchTouchEvent(MotionEvent ev) { if (!onFilterTouchEventForSecurity(ev)) { return false; } final int action = ev.getAction(); final float xf = ev.getX(); final float yf = ev.getY(); final float scrolledXFloat = xf + mScrollX; final float scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) {...} boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } final View target = mMotionTarget; if(target ==null ){...} if (!disallowIntercept && onInterceptTouchEvent(ev)) {...} if (isUpOrCancel) { mMotionTarget = null; } // finally offset the event to the target's coordinate system and // dispatch the event. final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc); return target.dispatchTouchEvent(ev); }
1.判斷當前是不是ACTION_UP
2.分別重置攔截標誌位以及將DOWN賦值的mMotionTarget置爲null,都UP了,固然置爲null,下一次DOWN還會再賦值的~最後,修改座標系統,而後調用target.dispatchTouchEvent(ev);
如今整個ViewGroup的事件分發流程的分析也就到此結束了,咱們最後再來簡單梳理一下吧:
ACTION_UP中,ViewGroup捕獲到事件,而後判斷是否攔截,若是沒有攔截,則直接調用mMotionTarget.dispatchTouchEvent(ev)固然了在分發以前都會修改下座標系統,把當前的x,y分別減去child.left 和 child.top ,而後傳給child;
3.1如何攔截
上面的總結都是基於:若是沒有攔截;那麼如何攔截呢?
複寫ViewGroup的onInterceptTouchEvent方法:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: //若是你以爲須要攔截 return true ; case MotionEvent.ACTION_MOVE: //若是你以爲須要攔截 return true ; case MotionEvent.ACTION_UP: //若是你以爲須要攔截 return true ; } return false; }
默認是不攔截的,即返回false;若是你須要攔截,只要return true就好了,這要該事件就不會往子View傳遞了,而且若是你在DOWN retrun true ,則DOWN,MOVE,UP子View都不會捕獲事件;若是你在MOVE return true , 則子View在MOVE和UP都不會捕獲事件。
緣由很簡單,當onInterceptTouchEvent(ev) return true的時候,會把mMotionTarget 置爲null ;
3.2如何不被攔截
若是ViewGroup的onInterceptTouchEvent(ev) 當ACTION_MOVE時return true ,即攔截了子View的MOVE以及UP事件;
此時子View但願依然可以響應MOVE和UP時該咋辦呢?
android給咱們提供了一個方法:requestDisallowInterceptTouchEvent(boolean) 用於設置是否容許攔截,咱們在子View的dispatchTouchEvent中直接這麼寫:
@Override public boolean dispatchTouchEvent(MotionEvent event) { getParent().requestDisallowInterceptTouchEvent(true); int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "dispatchTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "dispatchTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "dispatchTouchEvent ACTION_UP"); break; default: break; } return super.dispatchTouchEvent(event); }
getParent().requestDisallowInterceptTouchEvent(true); 這樣即便ViewGroup在MOVE的時候return true,子View依然能夠捕獲到MOVE以及UP事件。
ViewGroup MOVE和UP攔截的源碼是這樣的:
if (!disallowIntercept && onInterceptTouchEvent(ev)) { final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; ev.setAction(MotionEvent.ACTION_CANCEL); ev.setLocation(xc, yc); if (!target.dispatchTouchEvent(ev)) { // target didn't handle ACTION_CANCEL. not much we can do // but they should have. } // clear the target mMotionTarget = null; // Don't dispatch this event to our own view, because we already // saw it when intercepting; we just want to give the following // event to the normal onTouchEvent(). return true; }
當咱們把disallowIntercept設置爲true時,!disallowIntercept直接爲false,因而攔截的方法體就被跳過了
注:若是ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN裏面直接return true了,那麼子View是木有辦法的捕獲事件的
咱們的實例,直接點擊ViewGroup內的按鈕,固然直接很順利的走完整個流程;
可是有兩種特殊狀況
一、ACTION_DOWN的時候,子View.dispatchTouchEvent(ev)返回的爲false ;
若是你仔細看了,你會注意到ViewGroup的dispatchTouchEvent(ev)的ACTION_DOWN代碼是這樣的
if (child.dispatchTouchEvent(ev)) { // Event handled, we have a target now. mMotionTarget = child; return true; }
只有在child.dispatchTouchEvent(ev)返回true了,纔會認爲找到了可以處理當前事件的View,即mMotionTarget = child;
可是若是返回false,那麼mMotionTarget 依然是null
mMotionTarget 爲null會咋樣呢?
其實ViewGroup也是View的子類,若是沒有找到可以處理該事件的子View,或者乾脆就沒有子View;
那麼,它做爲一個View,就至關於View的事件轉發了~~直接super.dispatchTouchEvent(ev);
源碼是這樣的:
final View target = mMotionTarget; if (target == null) { // We don't have a target, this means we're handling the // event as a regular view. ev.setLocation(xf, yf); if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; } return super.dispatchTouchEvent(ev); }
咱們沒有一個可以處理該事件的目標元素,意味着咱們須要本身處理~~~就至關於傳統的View~
二、那麼何時子View.dispatchTouchEvent(ev)返回的爲true
若是你仔細看了上篇博客,你會發現只要子View支持點擊或者長按事件必定返回true~~
源碼是這樣的:
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { return true ; }
一、若是ViewGroup找到了可以處理該事件的View,則直接交給子View處理,本身的onTouchEvent不會被觸發;
二、能夠經過複寫onInterceptTouchEvent(ev)方法,攔截子View的事件(即return true),把事件交給本身處理,則會執行本身對應的onTouchEvent方法
三、子View能夠經過調用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup對其MOVE或者UP事件進行攔截;
好了,那麼實際應用中能解決哪些問題呢?好比你須要寫一個相似slidingmenu的左側隱藏menu,主Activity上有個Button、ListView或者任何能夠響應點擊的View,你在當前View上死命的滑動,菜單欄也出不來;由於MOVE事件被子View處理了~ 你須要這麼作:在ViewGroup的dispatchTouchEvent中判斷用戶是否是想顯示菜單,若是是,則在onInterceptTouchEvent(ev)攔截子View的事件;本身進行處理,這樣本身的onTouchEvent就能夠順利展示出菜單欄了~~
參考文章:http://blog.csdn.net/lmj623565791/article/details/39102591