1. 程式人生 > >android Button使用者互動——監聽機制呼叫過程

android Button使用者互動——監聽機制呼叫過程

一、使用者互動——即對使用者的操作進行響應

android的事件處理機制:

1.基於監聽的事件處理機制

2.基於回撥的事件處理機制

監聽機制的好處:傳統的順序程式設計總是按照流程來安排工作,而事件機制的特點在於:等待,如果有事情發生則處理,順序程式設計在沒有事情的時候也迴圈執行,做毫無效率的空迴圈工作。事件機制在沒有事情的時候可以不作任何事情等待,從而可以釋放各種資源用於其它需要的程式。

二、下面我們來分析一下View的原始碼,來解釋一下監聽機制:

比如說你當前有一個非常簡單的專案,只有一個Activity,並且Activity中只有一個按鈕。你可能已經知道,如果想要給這個按鈕註冊一個點選事件,只需要呼叫

button.setOnClickListener(new OnClickListener() {  
    @Override  
    public void onClick(View v) {  
        Log.d("TAG", "onClick execute");  
    }  
});  
setOnClickListener:註冊監聽器;

onClick:處理函式;

這樣在onClick方法裡面寫實現,就可以在按鈕被點選的時候執行。你可能也已經知道,如果想給這個按鈕再新增一個touch事件,只需要註冊另外一個監聽器。

button.setOnTouchListener(new OnTouchListener() {  
    @Override  
    public boolean onTouch(View v, MotionEvent event) {  
        Log.d("TAG", "onTouch execute, action " + event.getAction());  
        return false;  
    }  
});  
onTouch方法裡能做的事情比onClick要多一些,比如判斷手指按下、擡起、移動等事件。那麼如果我兩個事件都註冊了,哪一個會先執行呢?我們來試一下就知道了,執行程式點選按鈕,列印結果如下:


可以看到,onTouch是優先於onClick執行的,並且onTouch執行了兩次,一次是ACTION_DOWN,一次是ACTION_UP(你還可能會有多次ACTION_MOVE的執行,如果你手抖了一下)。因此事件傳遞的順序是先經過onTouch,再傳遞到onClick。

細心的朋友應該可以注意到,onTouch方法是有返回值的,這裡我們返回的是false,如果我們嘗試把onTouch方法裡的返回值改成true,再執行一次,結果如下:


如果到現在為止,以上的所有知識點你都是清楚的,那麼說明你對Android事件傳遞的基本用法應該是掌握了。不過別滿足於現狀,讓我們從原始碼的角度分析一下,出現上述現象的原理是什麼。

首先你需要知道一點,只要你觸控到了任何一個控制元件,就一定會呼叫該控制元件的dispatchTouchEvent方法。

那當我們去點選按鈕的時候,就會去呼叫Button類裡的dispatchTouchEvent方法,可是你會發現Button類裡並沒有這個方法,那麼就到它的父類TextView裡去找一找,你會發現TextView裡也沒有這個方法,那沒辦法了,只好繼續在TextView的父類View裡找一找,這個時候你終於在View裡找到了這個方法,示意圖如下:


然後我們來看一下View中dispatchTouchEvent方法的原始碼:

public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}  
這個方法非常的簡潔,只有短短几行程式碼!我們可以看到,在這個方法內,首先是進行了一個判斷,如果mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)這三個條件都為真,就返回true,否則就去執行onTouchEvent(event)方法並返回。

先看一下第一個條件,mOnTouchListener這個變數是在哪裡賦值的呢?我們尋找之後在View裡發現瞭如下方法:

public void setOnTouchListener(OnTouchListener l) {  
    mOnTouchListener = l;  
}  
Bingo!找到了,mOnTouchListener正是在setOnTouchListener方法裡賦值的,也就是說只要我們給控制元件註冊了touch事件,mOnTouchListener就一定被賦值了。

第二個條件(mViewFlags & ENABLED_MASK) == ENABLED是判斷當前點選的控制元件是否是enable的,按鈕預設都是enable的,因此這個條件恆定為true。

第三個條件就比較關鍵了,mOnTouchListener.onTouch(this, event),其實也就是去回撥控制元件註冊touch事件時的onTouch方法。也就是說如果我們在onTouch方法裡返回true,就會讓這三個條件全部成立,從而整個方法直接返回true。如果我們在onTouch方法裡返回false,就會再去執行onTouchEvent(event)方法。

現在我們可以結合前面的例子來分析一下了,首先在dispatchTouchEvent中最先執行的就是onTouch方法,因此onTouch肯定是要優先於onClick執行的,也是印證了剛剛的列印結果。而如果在onTouch方法裡返回了true,就會讓dispatchTouchEvent方法直接返回true,不會再繼續往下執行。而列印結果也證實瞭如果onTouch返回true,onClick就不會再執行了。

根據以上原始碼的分析,從原理上解釋了我們前面例子的執行結果。而上面的分析還透漏出了一個重要的資訊,那就是onClick的呼叫肯定是在onTouchEvent(event)方法中的!那我們馬上來看下onTouchEvent的原始碼,如下所示:

public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  
    if ((viewFlags & ENABLED_MASK) == DISABLED) {  
        // A disabled view that is clickable still consumes the touch  
        // events, it just doesn't respond to them.  
        return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
    }  
    if (mTouchDelegate != null) {  
        if (mTouchDelegate.onTouchEvent(event)) {  
            return true;  
        }  
    }  
    if (((viewFlags & CLICKABLE) == CLICKABLE ||  
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
        switch (event.getAction()) {  
            case MotionEvent.ACTION_UP:  
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
                if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
                    // take focus if we don't have it already and we should in  
                    // touch mode.  
                    boolean focusTaken = false;  
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
                        focusTaken = requestFocus();  
                    }  
                    if (!mHasPerformedLongPress) {  
                        // This is a tap, so remove the longpress check  
                        removeLongPressCallback();  
                        // Only perform take click actions if we were in the pressed state  
                        if (!focusTaken) {  
                            // Use a Runnable and post this rather than calling  
                            // performClick directly. This lets other visual state  
                            // of the view update before click actions start.  
                            if (mPerformClick == null) {  
                                mPerformClick = new PerformClick();  
                            }  
                            if (!post(mPerformClick)) {  
                                performClick();  
                            }  
                        }  
                    }  
                    if (mUnsetPressedState == null) {  
                        mUnsetPressedState = new UnsetPressedState();  
                    }  
                    if (prepressed) {  
                        mPrivateFlags |= PRESSED;  
                        refreshDrawableState();  
                        postDelayed(mUnsetPressedState,  
                                ViewConfiguration.getPressedStateDuration());  
                    } else if (!post(mUnsetPressedState)) {  
                        // If the post failed, unpress right now  
                        mUnsetPressedState.run();  
                    }  
                    removeTapCallback();  
                }  
                break;  
            case MotionEvent.ACTION_DOWN:  
                if (mPendingCheckForTap == null) {  
                    mPendingCheckForTap = new CheckForTap();  
                }  
                mPrivateFlags |= PREPRESSED;  
                mHasPerformedLongPress = false;  
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                break;  
            case MotionEvent.ACTION_CANCEL:  
                mPrivateFlags &= ~PRESSED;  
                refreshDrawableState();  
                removeTapCallback();  
                break;  
            case MotionEvent.ACTION_MOVE:  
                final int x = (int) event.getX();  
                final int y = (int) event.getY();  
                // Be lenient about moving outside of buttons  
                int slop = mTouchSlop;  
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                        (y < 0 - slop) || (y >= getHeight() + slop)) {  
                    // Outside button  
                    removeTapCallback();  
                    if ((mPrivateFlags & PRESSED) != 0) {  
                        // Remove any future long press/tap checks  
                        removeLongPressCallback();  
                        // Need to switch from pressed to not pressed  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                    }  
                }  
                break;  
        }  
        return true;  
    }  
    return false;  
}  

相較於剛才的dispatchTouchEvent方法,onTouchEvent方法複雜了很多,不過沒關係,我們只挑重點看就可以了。

首先在第14行我們可以看出,如果該控制元件是可以點選的就會進入到第16行的switch判斷中去,而如果當前的事件是擡起手指,則會進入到MotionEvent.ACTION_UP這個case當中。在經過種種判斷之後,會執行到第38行的performClick()方法,那我們進入到這個方法裡瞧一瞧:

public boolean performClick() {  
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  
    if (mOnClickListener != null) {  
        playSoundEffect(SoundEffectConstants.CLICK);  
        mOnClickListener.onClick(this);  
        return true;  
    }  
    return false;  
}  
可以看到,只要mOnClickListener不是null,就會去呼叫它的onClick方法,那mOnClickListener又是在哪裡賦值的呢?經過尋找後找到如下方法:
public void setOnClickListener(OnClickListener l) {  
    if (!isClickable()) {  
        setClickable(true);  
    }  
    mOnClickListener = l;  
}  


一切都是那麼清楚了!當我們通過呼叫setOnClickListener方法來給控制元件註冊一個點選事件時,就會給mOnClickListener賦值。然後每當控制元件被點選時,都會在performClick()方法裡回撥被點選控制元件的onClick方法。

這樣View的整個事件分發的流程就讓我們搞清楚了!不過別高興的太早,現在還沒結束,還有一個很重要的知識點需要說明,就是touch事件的層級傳遞。我們都知道如果給一個控制元件註冊了touch事件,每次點選它的時候都會觸發一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。這裡需要注意,如果你在執行ACTION_DOWN的時候返回了false,後面一系列其它的action就不會再得到執行了。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發後一個action。

說到這裡,很多的朋友肯定要有巨大的疑問了。這不是在自相矛盾嗎?前面的例子中,明明在onTouch事件裡面返回了false,ACTION_DOWN和ACTION_UP不是都得到執行了嗎?其實你只是被假象所迷惑了,讓我們仔細分析一下,在前面的例子當中,我們到底返回的是什麼。

參考著我們前面分析的原始碼,首先在onTouch事件裡返回了false,就一定會進入到onTouchEvent方法中,然後我們來看一下onTouchEvent方法的細節。由於我們點選了按鈕,就會進入到第14行這個if判斷的內部,然後你會發現,不管當前的action是什麼,最終都一定會走到第89行,返回一個true。

是不是有一種被欺騙的感覺?明明在onTouch事件裡返回了false,系統還是在onTouchEvent方法中幫你返回了true。就因為這個原因,才使得前面的例子中ACTION_UP可以得到執行。

那我們可以換一個控制元件,將按鈕替換成ImageView,然後給它也註冊一個touch事件,並返回false。如下所示:

imageView.setOnTouchListener(new OnTouchListener() {
	@Override
	public boolean onTouch(View v, MotionEvent event) {
		Log.d("TAG", "onTouch execute, action " + event.getAction());
		return false;
	}
});

執行一下程式,點選ImageView,你會發現結果如下:

在ACTION_DOWN執行完後,後面的一系列action都不會得到執行了。這又是為什麼呢?因為ImageView和按鈕不同,它是預設不可點選的,因此在onTouchEvent的第14行判斷時無法進入到if的內部,直接跳到第91行返回了false,也就導致後面其它的action都無法執行了。

好了,關於View的事件分發,我想講的東西全都在這裡了。現在我們再來回顧一下開篇時提到的那三個問題,相信每個人都會有更深一層的理解。

1. onTouch和onTouchEvent有什麼區別,又該如何使用?

從原始碼中可以看出,這兩個方法都是在View的dispatchTouchEvent中呼叫的,onTouch優先於onTouchEvent執行。如果在onTouch方法中通過返回true將事件消費掉,onTouchEvent將不會再執行。

另外需要注意的是,onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能為空,第二當前點選的控制元件必須是enable的。因此如果你有一個控制元件是非enable的,那麼給它註冊onTouch事件將永遠得不到執行。對於這一類控制元件,如果我們想要監聽它的touch事件,就必須通過在該控制元件中重寫onTouchEvent方法來實現。

2. 為什麼給ListView引入了一個滑動選單的功能,ListView就不能滾動了?

如果你閱讀了Android滑動框架完全解析,教你如何一分鐘實現滑動選單特效 這篇文章,你應該會知道滑動選單的功能是通過給ListView註冊了一個touch事件來實現的。如果你在onTouch方法裡處理完了滑動邏輯後返回true,那麼ListView本身的滾動事件就被遮蔽了,自然也就無法滑動(原理同前面例子中按鈕不能點選),因此解決辦法就是在onTouch方法裡返回false。

3. 為什麼圖片輪播器裡的圖片使用Button而不用ImageView?

提這個問題的朋友是看過了Android實現圖片滾動控制元件,含頁籤功能,讓你的應用像淘寶一樣炫起來 這篇文章。當時我在圖片輪播器裡使用Button,主要就是因為Button是可點選的,而ImageView是不可點選的。如果想要使用ImageView,可以有兩種改法。第一,在ImageView的onTouch方法裡返回true,這樣可以保證ACTION_DOWN之後的其它action都能得到執行,才能實現圖片滾動的效果。第二,在佈局檔案裡面給ImageView增加一個android:clickable="true"的屬性,這樣ImageView變成可點選的之後,即使在onTouch裡返回了false,ACTION_DOWN之後的其它action也是可以得到執行的。

今天的講解就到這裡了,相信大家現在對Android事件分發機制又有了進一步的認識,在後面的文章中我會再帶大家一起探究Android中ViewGroup的事件分發機制,感興趣的朋友請繼續閱讀 Android事件分發機制完全解析,帶你從原始碼的角度徹底理解(下) 。

記得在前面的文章中,我帶大家一起從原始碼的角度分析了Android中View的事件分發機制,相信閱讀過的朋友對View的事件分發已經有比較深刻的理解了。

那麼今天我們將繼續上次未完成的話題,從原始碼的角度分析ViewGruop的事件分發。

首先我們來探討一下,什麼是ViewGroup?它和普通的View有什麼區別?

顧名思義,ViewGroup就是一組View的集合,它包含很多的子View和子VewGroup,是Android中所有佈局的父類或間接父類,像LinearLayout、RelativeLayout等都是繼承自ViewGroup的。但ViewGroup實際上也是一個View,只不過比起View,它多了可以包含子View和定義佈局引數的功能。ViewGroup繼承結構示意圖如下所示:


可以看到,我們平時專案裡經常用到的各種佈局,全都屬於ViewGroup的子類。

簡單介紹完了ViewGroup,我們現在通過一個Demo來演示一下Android中VewGroup的事件分發流程吧。

首先我們來自定義一個佈局,命名為MyLayout,繼承自LinearLayout,如下所示:

public class MyLayout extends LinearLayout {

	public MyLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

}
然後,開啟主佈局檔案activity_main.xml,在其中加入我們自定義的佈局:
<com.example.viewgrouptouchevent.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/my_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Button2" />

</com.example.viewgrouptouchevent.MyLayout>
可以看到,我們在MyLayout中添加了兩個按鈕,接著在MainActivity中為這兩個按鈕和MyLayout都註冊了監聽事件:
myLayout.setOnTouchListener(new OnTouchListener() {
	@Override
	public boolean onTouch(View v, MotionEvent event) {
		Log.d("TAG", "myLayout on touch");
		return false;
	}
});
button1.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v) {
		Log.d("TAG", "You clicked button1");
	}
});
button2.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v) {
		Log.d("TAG", "You clicked button2");
	}
});
我們在MyLayout的onTouch方法,和Button1、Button2的onClick方法中都列印了一句話。現在執行一下專案,效果圖如下所示:

分別點選一下Button1、Button2和空白區域,列印結果如下所示:

你會發現,當點選按鈕的時候,MyLayout註冊的onTouch方法並不會執行,只有點選空白區域的時候才會執行該方法。你可以先理解成Button的onClick方法將事件消費掉了,因此事件不會再繼續向下傳遞。

那就說明Android中的touch事件是先傳遞到View,再傳遞到ViewGroup的?現在下結論還未免過早了,讓我們再來做一個實驗。

查閱文件可以看到,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!

好吧,既然是布林型的返回,那麼只有兩種可能,我們在MyLayout中重寫這個方法,然後返回一個true試試,程式碼如下所示:

public class MyLayout extends LinearLayout {

	public MyLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return true;
	}
	
}
現在再次執行專案,然後分別Button1、Button2和空白區域,列印結果如下所示:

你會發現,不管你點選哪裡,永遠都只會觸發MyLayout的touch事件了,按鈕的點選事件完全被遮蔽掉了!這是為什麼呢?如果Android中的touch事件是先傳遞到View,再傳遞到ViewGroup的,那麼MyLayout又怎麼可能遮蔽掉Button的點選事件呢?

看來只有通過閱讀原始碼,搞清楚Android中ViewGroup的事件分發機制,才能解決我們心中的疑惑了,不過這裡我想先跟你透露一句,Android中touch事件的傳遞,絕對是先傳遞到ViewGroup,再傳遞到View的。記得在Android事件分發機制完全解析,帶你從原始碼的角度徹底理解(上) 中我有說明過,只要你觸摸了任何控制元件,就一定會呼叫該控制元件的dispatchTouchEvent方法。這個說法沒錯,只不過還不完整而已。實際情況是,當你點選了某個控制元件,首先會去呼叫該控制元件所在佈局的dispatchTouchEvent方法,然後在佈局的dispatchTouchEvent方法中找到被點選的相應控制元件,再去呼叫該控制元件的dispatchTouchEvent方法。如果我們點選了MyLayout中的按鈕,會先去呼叫MyLayout的dispatchTouchEvent方法,可是你會發現MyLayout中並沒有這個方法。那就再到它的父類LinearLayout中找一找,發現也沒有這個方法。那隻好繼續再找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);
}
這個方法程式碼比較長,我們只挑重點看。首先在第13行可以看到一個條件判斷,如果disallowIntercept和!onInterceptTouchEvent(ev)兩者有一個為true,就會進入到這個條件判斷中。disallowIntercept是指是否禁用掉事件攔截的功能,預設是false,也可以通過呼叫requestDisallowInterceptTouchEvent方法對這個值進行修改。那麼當第一個值為false的時候就會完全依賴第二個值來決定是否可以進入到條件判斷的內部,第二個值是什麼呢?竟然就是對onInterceptTouchEvent方法的返回值取反!也就是說如果我們在onInterceptTouchEvent方法中返回false,就會讓第二個值為true,從而進入到條件判斷的內部,如果我們在onInterceptTouchEvent方法中返回true,就會讓第二個值為false,從而跳出了這個條件判斷。

這個時候你就可以思考一下了,由於我們剛剛在MyLayout中重寫了onInterceptTouchEvent方法,讓這個方法返回true,導致所有按鈕的點選事件都被遮蔽了,那我們就完全有理由相信,按鈕點選事件的處理就是在第13行條件判斷的內部進行的!

那我們重點來看下條件判斷的內部是怎麼實現的。在第19行通過一個for迴圈,遍歷了當前ViewGroup下的所有子View,然後在第24行判斷當前遍歷的View是不是正在點選的View,如果是的話就會進入到該條件判斷的內部,然後在第29行呼叫了該View的dispatchTouchEvent,之後的流程就和 Android事件分發機制完全解析,帶你從原始碼的角度徹底理解(上) 中講解的是一樣的了。我們也因此證實了,按鈕點選事件的處理確實就是在這裡進行的。

然後需要注意一下,呼叫子View的dispatchTouchEvent後是有返回值的。我們已經知道,如果一個控制元件是可點選的,那麼點選該控制元件時,dispatchTouchEvent的返回值必定是true。因此會導致第29行的條件判斷成立,於是在第31行給ViewGroup的dispatchTouchEvent方法直接返回了true。這樣就導致後面的程式碼無法執行到了,也是印證了我們前面的Demo列印的結果,如果按鈕的點選事件得到執行,就會把MyLayout的touch事件攔截掉。

那如果我們點選的不是按鈕,而是空白區域呢?這種情況就一定不會在第31行返回true了,而是會繼續執行後面的程式碼。那我們繼續往後看,在第44行,如果target等於null,就會進入到該條件判斷內部,這裡一般情況下target都會是null,因此會在第50行呼叫super.dispatchTouchEvent(ev)。這句程式碼會呼叫到哪裡呢?當然是View中的dispatchTouchEvent方法了,因為ViewGroup的父類就是View。之後的處理邏輯又和前面所說的是一樣的了,也因此MyLayout中註冊的onTouch方法會得到執行。之後的程式碼在一般情況下是走不到的了,我們也就不再繼續往下分析。

再看一下整個ViewGroup事件分發過程的流程圖吧,相信可以幫助大家更好地去理解:

    


現在整個ViewGroup的事件分發流程的分析也就到此結束了,我們最後再來簡單梳理一下吧。

1. Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View的。

2. 在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true代表不允許事件繼續向子View傳遞,返回false代表不對事件進行攔截,預設返回false。

3. 子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件。

好了,Android事件分發機制完全解析到此全部結束,結合上下兩篇,相信大家對事件分發的理解已經非常深刻了。





  

相關推薦

android Button使用者互動——機制呼叫過程

一、使用者互動——即對使用者的操作進行響應 android的事件處理機制: 1.基於監聽的事件處理機制 2.基於回撥的事件處理機制 監聽機制的好處:傳統的順序程式設計總是按照流程來安排工作,而事件機制的特點在於:等待,如果有事情發生則處理,順序程式設計在沒有事情的時候也迴圈

android button雙擊事件

第一次做按鈕雙擊監聽事件,在這裡就分享出來我自己的做法 剛開始我直接這樣做的: private int count = 0; button.setOnClickListener(new OnClick

Android中各種訊息機制

1、Boadcast, receiverboadcast:sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));receiver:IntentFilter filter = new IntentFilter();        filter

Android與JS互動,url實時變化的方法.

private class MyWebViewClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view,

dispatchTouchEvent,onUserInteraction,onTouchEvent 呼叫時序(按鍵函式呼叫過程

一、只有一個Activity時:一次拇指點選事件(包括ACTION_DOWN,ACTION_UP),呼叫時序如下: dispatchTouchEvent onUserInteraction onTouchEvent (ACTION_DOWN) dispatchTo

Java事件機制與觀察設計模式

idea demo1 script 操作 alt face ner over 方法 一. Java事件監聽機制 1. 事件監聽三要素: 事件源,事件對象,事件監聽器 2. 三要素之間的關系:事件源註冊事件監聽器後,當事件源上發生某個動作時,事件源就會調用事件監聽的一個方法,

java中的機制——觀察設計模式詳解

好久沒有做筆記了,我發現研究過一個東西后忘的很快,等下次遇到後還得重新查資料學,倒不如寫篇筆記記錄一下下次遇到相同的問題後查查筆記瞬間就知道了,從而節省了好多時間,今天心血來潮哈哈,寫一篇簡單的筆記

java事件機制(觀察設計模式的實際運用)

package cn.yang.test.controller; /**java的事件監聽機制和觀察者設計模式 * Created by Dev_yang on 2016/3/1. */ publ

SpringBoot事件機制及觀察模式/釋出訂閱模式

[toc] ## 本篇要點 - 介紹觀察者模式和釋出訂閱模式的區別。 - SpringBoot快速入門事件監聽。 ## 什麼是觀察者模式? 觀察者模式是經典行為型設計模式之一。 在GoF的《設計模式》中,觀察者模式的定義:**在物件之間定義一個一對多的依賴,當一個物件狀態改變的時候,所有依賴的物件都會自

java中的key事件機制

com java.awt imp package 時間 ext javax .get pri package com.at221; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; im

Java 中的事件機制

add import userdata 開發人員 util ner dns pre sta 看項目代碼時遇到了好多事件監聽機制相關的代碼。現學習一下: java事件機制包含三個部分:事件、事件監聽器、事件源。 1.事件:繼承自java.util.EventO

Java中的事件機制

void 初始化 release vax 輸入 logs p s get location 鼠標事件監聽機制的三個方面: 1.事件源對象:   事件源對象就是能夠產生動作的對象。在Java語言中所有的容器組件和元素組件都是事件監聽中的事件源對象。Java中根據事件的動作來區

Android通過Intent.ACTION_CLOSE_SYSTEM_DIALOGSHome按鍵消息

後門 str eve log reason anr rri dem .sh Android對屏幕下方經常使用的四個按鍵消息處理是不一致的: 1、搜索按鍵的消息在onKeyDown或者onKeyUp中接收; 2、菜單按鍵的消息在onCreateOptionsMen

JAVA 圖形開發之計算器設計(事件機制

oncommand image 事件監聽 str one 創建 dac orm mat /*文章中用到的代碼只是一部分,需要源碼的可通過郵箱聯系我 [email protected]*/ 前段時間剛幫同學用MFC寫了個計算器,現在學到JAVA的圖形開發,就試著水了一個計算器

spring事件機制

ide could 事情 task object 負責 his try and 事件機制的主要成員:   事件 事件監聽器(監聽事件觸發,處理一些事情) 事件源(發布事件) javaSE 提供了一系列自定義事件的標準。 EvenObject,為javaSE提供

UGUI的事件機制

UGUI控制元件的事件響應有很多種方式,比如使用元件EventTrigger來新增事件監聽,或者實現IDragHandler等介面,或者更直接地繼承EventTrigger來進行更靈話的呼叫。 下面分別就上面3種監聽方式進行講解,這些例子都是實現拖動Image的功能。 一、使用元件Event

自定義JAVA事件機制

JAVA中的事件機制的參與者有3種角色:Event、EventSource、Listener 1.Event:就是事件產生時具體的“事件”,用於Listener的相應的方法之中,作為引數一般存在於Listener的方法之中。 2.EventSource:事件源,它的作用主要是對事件和監聽進行

ZooKeeper Watcher機制(資料變更的通知)(二)(分析)

緊接著上一篇部落格:https://blog.csdn.net/Dongguabai/article/details/82970852 在輸出內容中有這樣兩個結果: 在ZooKeeper中,介面類Watcher用於表示一個標準的事件處理器,其定義了事件通知相關的邏輯,包含Ke

深入理解Spring的容器內事件釋出機制

目錄 1. 什麼是事件監聽機制 2. JDK中對事件監聽機制的支援 2.1 基於JDK實現對任務執行結果的監聽 3.Spring容器對事件監聽機制的支援 3.1 基於Spring實現對任務執行結果的監聽 4.Spring事件監聽原始碼解析

深入理解Spring的容器內事件發布機制

not main alt 事件類型 http inner interface 改變 pear 目錄 1. 什麽是事件監聽機制 2. JDK中對事件監聽機制的支持 2.1 基於JDK實現對任務執行結果的監聽 3.Spring容器對事件監聽機制的支持 3.1 基於Sprin