上一篇我們介紹了View的事件分發(fā)機制叠赦,相信大家對View的事件分發(fā)一定都有了很深的理解了。之前也曾提到西疤,Android的事件分發(fā)機制由兩部分組成币绩,分別是View的事件分發(fā)機制以及ViewGroup的事件分發(fā)機制,今天就趁熱打鐵瘸羡,帶領大家從源代碼的級別深入探究一下ViewGroup的事件分發(fā)機制漩仙,盡可能地讓大家對Android的事件分發(fā)機制有一個全面而透徹的理解,好了犹赖,話不多說队他,讓我們開啟美妙的探索之旅吧_
既然我們從View的事件分發(fā)延伸到了ViewGroup的事件分發(fā),那便不得不談一下View峻村,ViewGroup之間的區(qū)別與聯(lián)系了麸折。我們先來看一下Google官方文檔對ViewGroup的闡述:
顯而易見,ViewGroup繼承自View粘昨,說明ViewGroup本身也是一個View垢啼。再看Class Overview中的解釋:
A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines the ViewGroup.LayoutParams class which serves as the base class for layouts parameters.
講得也非常清楚,ViewGroup是一個很特殊的View雾棺,其相對于View多了包含子View膊夹,子ViewGroup以及定義布局參數(shù)的功能。
弄清楚了View與ViewGroup之間的關系捌浩,我們先通過一段Demo代碼放刨,讓大家對ViewGroup的事件分發(fā)機制有一個直觀的了解。
首先尸饺,我們自定義一個布局類进统,并讓它繼承自LinearLayout助币,自定義布局類的目的是為了能夠重寫布局類中與ViewGroup的事件分發(fā)有關的方法,自定義布局類的代碼如下:
public class CustomLayout extends LinearLayout {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
MainActivity對應的布局文件:
<com.example.eventdispatch.CustomLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/customLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.eventdispatch.MainActivity" >
<Button
android:id="@+id/btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button1"
/>
<Button
android:id="@+id/btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button2"
/>
</com.example.eventdispatch.CustomLayout>
在MainActivity中螟碎,我們給CustomLayout對象設置了Touch事件眉菱,給兩個Button對象設置了Click事件,MainActivity對應的代碼如下:
public class MainActivity extends ActionBarActivity {
private CustomLayout customLayout;
private Button btn1;
private Button btn2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
customLayout=(CustomLayout)findViewById(R.id.customLayout);
btn1=(Button)findViewById(R.id.btn1);
btn2=(Button)findViewById(R.id.btn2);
customLayout.setOnTouchListener(new OnTouchListener(){
@Override
public boolean onTouch(View arg0, MotionEvent arg1) {
Log.v("TAG","customLayout onTouch");
return false;
}
});
btn1.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
Log.v("TAG","btn1 onClick");
}
});
btn2.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
Log.v("TAG","btn2 onClick");
}
});
}
}
我們點擊btn1掉分,輸出如下:
我們點擊btn2俭缓,輸出如下:
我們點擊空白區(qū)域,輸出如下:
可以發(fā)現(xiàn)酥郭,我們點擊按鈕時僅僅是調(diào)用了按鈕本身的Click事件华坦,并沒有去調(diào)用按鈕所在布局的Touch事件,那么不从,這是否可以說明Android中的事件是先傳遞到View惜姐,再傳遞到ViewGroup的呢?
先別著急下結論椿息,我們再做一個實驗歹袁。我們發(fā)現(xiàn),ViewGroup中有一個叫做onInterceptTouchEvent的方法寝优,我們來看一下這個方法的源代碼:
/**
* Implement this method to intercept all touch screen motion events. This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way. Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it). Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
注釋寫了一大堆条舔,結果卻是默認返回一個false!
我們在CustomLayout中重寫onInterceptTouchEvent方法倡勇,讓它返回一個true試試看:
public class CustomLayout extends LinearLayout {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev){
return true;
}
}
再次運行程序逞刷,點擊btn1,輸出如下:
點擊btn2,輸出如下:
點擊空白區(qū)域,輸出如下:
我們發(fā)現(xiàn)妻熊,將onInterceptTouchEvent方法的返回值改為true之后,無論點擊btn1,btn2還是空白區(qū)域仑最,都只會觸發(fā)Layout的Touch事件扔役,如果說事件是從View傳遞到ViewGroup的,那么ViewGroup怎么可能攔截掉View的事件呢警医?看來亿胸,只有源碼才能告訴我們答案了。
上篇文章曾經(jīng)提到過预皇,在Android中侈玄,只要觸摸到了任何控件,都會去調(diào)用這個控件的dispatchTouchEvent方法吟温,其實這個說法并不準確序仙,更加準確的說法是,觸摸到了任何控件鲁豪,都會首先去調(diào)用控件所在布局的dispatchTouchEvent方法潘悼,然后在控件所在布局的dispatchTouchEvent方法中律秃,遍歷所有的控件,找出當前點擊的控件治唤,調(diào)用其dispatchTouchEvent方法棒动。
在CustomLayout中點擊Button時,會先去調(diào)用CustomLayout的dispatchTouchEvent方法宾添,我們發(fā)現(xiàn)CustomLayout中是沒有這個方法的船惨,我們到CustomLayout的父類LinearLayout找一下,發(fā)現(xiàn)沒有這個方法缕陕,我們再到LinearLayout的父類ViewGroup中找一下粱锐,終于,我們在ViewGroup中找到了dispatchTouchEvent方法榄檬。
ViewGroup的dispatchTouchEvent方法如下:
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;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
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)) {
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)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
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);
}
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)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}
ViewGroup的dispatchTouchEvent方法很長卜范,我們先去看if (disallowIntercept || !onInterceptTouchEvent(ev))這一句,第一個判斷條件是disallowIntercept鹿榜,它是一個布爾變量海雪,代表是否禁用掉事件攔截功能,默認值為false舱殿,那么能不能進入這個if判斷就完全依賴于第二個判斷條件了奥裸,第二個判斷條件是!onInterceptTouchEvent(ev),就是對onInterceptTouchEvent方法的返回值取反沪袭。如果onInterceptTouchEvent的返回值為true湾宙,就無法進入該if判斷中,事件就無法傳遞到子View中(進入了該if判斷冈绊,事件才能往子View傳遞侠鳄,大家先暫時這樣理解著)。
我們接著去看看這個if判斷中做了什么事情死宣。從** for (int i = count - 1; i >= 0; i--) ** 這一句開始看伟恶,它會去遍歷所有子View,找出當前正在點擊的View,調(diào)用該View的dispatchTouchEvent方法毅该,如果該View的dispatchTouchEvent方法返回true博秫,則整個 ViewGroup的dispatchTouchEvent方法直接返回true,ViewGroup設置的事件便得不到處理了眶掌。
由上篇文章可知挡育,如果一個控件是可點擊的,那么點擊它朴爬,它的dispatchTouchEvent方法定然是返回true的即寒,現(xiàn)在我們可以回過頭來分析下之前的Demo代碼了,當CustomLayout 中的onInterceptTouchEvent方法返回false時(默認情況),點擊按鈕蒿叠,首先回去調(diào)用按鈕所在布局的dispatchTouchEvent方法明垢,在if (disallowIntercept || !onInterceptTouchEvent(ev))處,因為當前onInterceptTouchEvent返回false市咽,取反為true痊银,所以能進入到該if判斷中,事件便從我們的ViewGroup傳遞到子View中了施绎,之后溯革,找到當前點擊的按鈕,調(diào)用其dispatchTouchEvent方法谷醉,因為按鈕是可點擊的致稀,所以按鈕的dispatchTouchEvent方法會返回true,從而導致ViewGroup的dispatchTouchEvent方法直接返回true俱尼,CustomLayout中的Touch事件自然得不到執(zhí)行了抖单。如果當前點擊的是空白區(qū)域呢?那自然不會像剛才一樣直接返回true的遇八,代碼會繼續(xù)向下執(zhí)行矛绘,我們看到下面這段代碼:
if (target == null) {
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);
}
它會去判斷target是否為null,一般情況下target都是為null的刃永,之后便會去調(diào)用super.dispatchTouchEvent(ev)方法货矮。為啥要調(diào)super.dispatchTouchEvent(ev)方法呢?因為我們的ViewGroup本身就是一個View,調(diào)用super.dispatchTouchEvent(ev)方法就是去處理ViewGroup本身設置的一些事件。所以实檀,當我們點擊空白區(qū)域時,CustomLayout中的Touch事件會被執(zhí)行抓督。
理解了onInterceptTouchEvent方法返回false時的運行過程,再去分析onInterceptTouchEvent方法返回true時的輸出結果就是小菜一碟了束亏。當onInterceptTouchEvent方法返回true時本昏,if (disallowIntercept || !onInterceptTouchEvent(ev))這個判斷肯定是進不去的,之后便會執(zhí)行到super.dispatchTouchEvent(ev)枪汪,所以,無論是點擊Button怔昨,還是點擊空白區(qū)域雀久,都只會調(diào)用CustomLayout的Touch事件。
到這里趁舀,View的事件分發(fā)機制與ViewGroup的事件分發(fā)機制的源碼解析就基本結束了赖捌,可以看到,這兩者是緊密聯(lián)系,密不可分的越庇,真正的項目中也可能會有各種涉及到事件分發(fā)的復雜業(yè)務場景罩锐,但只要熟悉源碼,我們便所向披靡卤唉,無所畏懼涩惑,任憑業(yè)務場景千變?nèi)f化,我們都能妥善處理好事件分發(fā)的相關問題桑驱!