Android事件分發(fā)機制深度解析(ViewGroup篇)

上一篇我們介紹了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ā)的相關問題桑驱!

參考:http://blog.csdn.net/guolin_blog/article/details/9153747

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末竭恬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子熬的,更是在濱河造成了極大的恐慌痊硕,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件押框,死亡現(xiàn)場離奇詭異岔绸,居然都是意外死亡,警方通過查閱死者的電腦和手機橡伞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門盒揉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人骑歹,你說我怎么就攤上這事预烙。” “怎么了道媚?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵扁掸,是天一觀的道長。 經(jīng)常有香客問我最域,道長谴分,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任镀脂,我火速辦了婚禮牺蹄,結果婚禮上,老公的妹妹穿的比我還像新娘薄翅。我一直安慰自己沙兰,他們只是感情好,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布翘魄。 她就那樣靜靜地躺著鼎天,像睡著了一般。 火紅的嫁衣襯著肌膚如雪暑竟。 梳的紋絲不亂的頭發(fā)上斋射,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音,去河邊找鬼罗岖。 笑死涧至,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的桑包。 我是一名探鬼主播南蓬,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼捡多!你這毒婦竟也來了蓖康?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤垒手,失蹤者是張志新(化名)和其女友劉穎蒜焊,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體科贬,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡泳梆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了榜掌。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片优妙。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖憎账,靈堂內(nèi)的尸體忽然破棺而出套硼,到底是詐尸還是另有隱情,我是刑警寧澤胞皱,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布邪意,位于F島的核電站,受9級特大地震影響反砌,放射性物質(zhì)發(fā)生泄漏雾鬼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一宴树、第九天 我趴在偏房一處隱蔽的房頂上張望策菜。 院中可真熱鬧,春花似錦酒贬、人聲如沸又憨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽竟块。三九已至,卻和暖如春耐齐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工埠况, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留耸携,地道東北人。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓辕翰,卻偏偏與公主長得像夺衍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子喜命,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容