Android事件分發機制詳解

距離上篇文章有半個月,感受有些荒度,不過這篇文章寫下來確實是用了半個月,確實是小白,因此看了好多文章纔算是搞懂了事件分發。這篇文章更像是總結,把別人的東西比較清晰易懂的點本身記錄下來,再經過本身的實踐來了解事件分發的過程,廢話很少說,帶你看看小白是怎麼一步步瞭解事件分發的。java

1. 什麼是事件

要了解事件分發,那咱們先說說什麼是事件,其實這裏的事件指的就是點擊事件,當用戶觸摸屏幕的時候,將會產生點擊事件(Touch事件)android

Touch事件的相關細節(發生觸摸的位置、時間等)被封裝成MotionEvent對象bash

MotionEvent事件類型app

事件類型 具體動做
MotionEvent.ACTION_DOWN 按下View(全部事件的開始)
MotionEvent.ACTION_UP 擡起View(與DOWN對應)
MotionEvent.ACTION_MOVE 滑動View
MotionEvent.ACTION_CANCEL 結束事件(非人爲緣由)

事件序列:其實就是從手指觸摸屏幕到離開屏幕所發生的一系列事件ide

2. 什麼是事件分發

咱們要講的事件分發其實就是將點擊事件傳遞到某個具體的View,這個傳遞的過程就叫作事件分發函數

3. 事件在哪些對象間進行傳遞、順序是什麼

ActivityUI界面由ActivityViewGroupView及其派生類組成佈局

事件分發在這三個對象之間進行傳遞。post

當點擊事件發生後,事件先傳到Activity,再傳到ViewGroup,最終傳到View動畫

4. 事件分發有啥用?

默認狀況下事件分發會按照由ActivityViewGroup再到View的順序進行分發,當咱們不想View進行處理,讓ViewGroup處理,那就能夠進行攔截,這些知識能夠用於解決滑動衝突。this

例如:外部滑動和內部滑動方向不一致,當ScrollView嵌套Fragment,且Fragemnt內部有個豎向的ListView,當用戶左右滑動時,要讓外部的View攔截單擊事件,當用戶上下滑動時,要讓內部的View攔截點擊事件。怎麼攔截,在哪裏攔截,就用到了咱們這篇文章所講的內容了。

5. 事件分發涉及到的函數及相應的做用

方法 做用
dispatchTouchEvent 進行事件分發
onInterceptTouchEvent 事件攔截
onTouchEvent 事件消耗(就是交給當前View處理)
  • dispatchTouchEvent: 用來進行事件分發,若事件可以傳遞到當前View,則此方法必定會被調用。
  • onInterceptTouchEvent:dispatchTouchEvent方法內被調用,用來判斷是否攔截某個事件。若當前View攔截了某個事件,則該方法不會再被調用,返回結果表示是否攔截當前事件,該方法只在ViewGroup中存在。
  • onTouchEvent: 用來處理點擊事件,返回結果表示是否消耗當前事件,若不消耗,則在同一事件序列中,當前View沒法再次接收到事件。

這三個方法可用如下僞代碼表示

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    }else{
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}
複製代碼

對應的根ViewGroup,當一個點擊事件產生時,Activity會傳遞給它,這時它的dispatchTouchEvent就會被調用,若該ViewGrouponInterceptTouchEvent返回true,表明攔截該事件,可是否消耗該事件,還要看它的onTouchEvent的返回值,若是不攔截,則表明將事件分發下去給子View,接着子ViewdispatchTouchEvent方法會被調用,如此反覆直到事件被最終處理。

6. Activity的事件分發

當一個點擊事件發生時,事件最早傳到ActivitydispatchTouchEvent()進行事件分發。這裏主要要弄明白Activity是怎麼將事件分發到ViewGroup中的

6.1 Demo演示

咱們先看一個案例

(1) 自定義一個MyViewGroup,繼承自ViewGroup,重寫dispatchTouchEvent()方法

public class MyViewGroup extends ViewGroup{
    
    public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.i(TAG, "dispatchTouchEvent: ");        
		//這裏咱們暫時先返回false 
		return false;
    }
}
複製代碼

(2)Activity的佈局中,使用該佈局做爲最外層佈局

<com.ld.eventdispatchdemo.activitydispatch.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/myViewGroup"
    android:orientation="vertical"
    tools:context=".activitydispatch.ActivityDispatchActivity">

</com.ld.eventdispatchdemo.activitydispatch.MyViewGroup>
複製代碼

(3) 重寫該ActivitydispatchTouchEvent()onTouchEvent()方法,打印log日誌

public class Activity extends AppCompatActivity{
    ...
	private static final String TAG = "Activit_activitydispatch";
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if(ev.getAction()==MotionEvent.ACTION_DOWN){
            Log.i(TAG, "dispatchTouchEvent: ");
        }
     	//這裏是仿照源碼的格式寫的
        if(getWindow().superDispatchTouchEvent(ev)){
            Log.i(TAG, "dispatchTouchEvent: 這裏被調用");
            return true;
        }        
        return onTouchEvent(ev);
    }       
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(TAG, "onTouchEvent: ");
        return super.onTouchEvent(event);
    }
}
複製代碼

MyViewGroupdispatchTouchEvent返回false時,打印的log日誌爲:

Activit_activitydispatch: dispatchTouchEvent: 
MyViewGroup_activitydispatch: dispatchTouchEvent: 
Activit_activitydispatch: onTouchEvent: 
Activit_activitydispatch: onTouchEvent: 
Activit_activitydispatch: onTouchEvent: 
Activit_activitydispatch: onTouchEvent: 
Activit_activitydispatch: onTouchEvent: 
複製代碼

MyViewGroupdispatchTouchEvent返回true時,打印的log日誌爲:

Activit_activitydispatch: dispatchTouchEvent: 
MyViewGroup_activitydispatch: dispatchTouchEvent: 
Activit_activitydispatch: dispatchTouchEvent: 這裏被調用
MyViewGroup_activitydispatch: dispatchTouchEvent: 
Activit_activitydispatch: dispatchTouchEvent: 這裏被調用
MyViewGroup_activitydispatch: dispatchTouchEvent: 
Activit_activitydispatch: dispatchTouchEvent: 這裏被調用
MyViewGroup_activitydispatch: dispatchTouchEvent: 
Activit_activitydispatch: dispatchTouchEvent: 這裏被調用
複製代碼

仔細觀察能夠看到,當MyViewGroup的dispatchTouchEvent返回false時,Activity的onTouchEvent會被調用,返回true時,不會被調用,這是什麼緣由呢?

你可能會有疑問,個人ActivitydispatchTouchEvent()方法內爲什麼要這樣寫呢?別急,看完下面的源碼你就知道了。

6.2 源碼解析

目的:

一、研究MyViewGroupdispatchTouchEvent返回false時,ActivityonTouchEvent會被調用,返回true時,不會被調用的緣由

Activity中的dispatchTouchEvent()源碼以下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        //該方法爲空方法,不用管它
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
複製代碼

有沒有很熟悉,爲了方便觀察打日誌因此上面咱們重寫了ActivitydispatchTouchEvent方法,但內容和源碼基本一致。

能夠從源碼看到,當getWindow().superDispatchTouchEvent(ev)==true時,那麼此時return ture,天然就不會調用底下的onTouchEvent()方法,即ActivityonTouchEvent()

getWindow()返回Window對象,Window是抽象類,而PhoneWindowWindow的惟一實現類,因此getWindow().superDispatchTouchEvent(ev)其實就是調用的PhoneWindow內的superDispatchTouchEvent(ev)方法。

看看PhoneWindow類源碼:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
複製代碼

PhoneWindow將事件直接傳遞給了DecorView,接下來看看DecorView是什麼

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
                     
}
複製代碼

mDecorgetWindow().getDecorView()返回的View,經過setContentView設置的View是該View的子View

DecorView繼承自FrameLayout(ViewGroup),因此mDecor.superDispatchTouchEvent(event)其實調用的就是ViewGroupdispatchTouchEvent()方法,因此到這裏你就懂了吧,

if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
}
複製代碼

其實就至關於下面這個

if(viewgroup.DispatchTouchEvent(ev)){
    return true;
}
複製代碼

因此說當咱們的MyLayoutDispatchTouchEvent()返回true時,ActivityonTouchEvent就不會被調用。

6.3 事件時怎麼從Activity分發到ViewGroup中的

從上面ActivitydispatchTouchEvent源碼可知道,默認狀態下,它內部必定會調用該方法,而if()條件中的內容其實就是調用ViewGroupdispatchTouchEvent()方法,也就是在這裏完成了ActivityViewGroup的事件分發。

if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
}
複製代碼

6.4 小結:Activity分發的流程圖

7. ViewGroup的事件分發

上面講了ActivitydispatchTouchEvent內將事件傳遞到了ViewGroupdispatchTouchEvent()方法中,那麼ViewGroup又是如何將事件進一步向下分發的呢?

7.1 Demo演示

(1) 自定義MyLayout,繼承自LinearLayout,重寫onInterceptTouchEvent()方法,並返回true,重寫dispatchTouchEvent()onTouchEvent()方法,打印log日誌

public class MyLayout extends LinearLayout {
    
    private static final String TAG = "MyLayout_ViewGroupDispatch";
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i(TAG, "dispatchTouchEvent: ");
        return super.dispatchTouchEvent(ev);
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i(TAG, "onInterceptTouchEvent: ");
        //此處暫時返回true觀察現象
        return true;
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(TAG, "onTouchEvent: ");
        return super.onTouchEvent(event);
    }   
}
複製代碼

(2)Activity的佈局中,使用該佈局做爲最外層佈局,並在該佈局內添加一個按鈕

<com.ld.eventdispatchdemo.viewgroupdispatch.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:id="@+id/myLayout"
    tools:context=".viewgroupdispatch.ViewGroupActivity">
    <Button
        android:id="@+id/btn1"
        android:text="Button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />  
</com.ld.eventdispatchdemo.viewgroupdispatch.MyLayout>
複製代碼

(3)Activity內爲按鈕添加點擊事件

public class ViewGroupActivity extends AppCompatActivity {
    
    private Button btn1;
    private static final String TAG = "Activity_ViewGroupDispatch";
	@Override
    protected void onCreate(Bundle savedInstanceState) {
        
        btn1 = findViewById(R.id.btn1);
        btn1.setOnClickListener(new View.OnClickListener() {
            @SuppressLint("LongLogTag")
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick: 點擊了按鈕1");
            }
        });
    }           
}
複製代碼

MyLayoutonInterceptTouchEvent()返回true時,分別點擊空白處、點擊按鈕,log日誌以下:

//點擊空白處
MyLayout_ViewGroupDispatch: dispatchTouchEvent: 
MyLayout_ViewGroupDispatch: onInterceptTouchEvent: 
MyLayout_ViewGroupDispatch: onTouchEvent: 
//點擊按鈕
MyLayout_ViewGroupDispatch: dispatchTouchEvent: 
MyLayout_ViewGroupDispatch: onInterceptTouchEvent: 
MyLayout_ViewGroupDispatch: onTouchEvent: 
複製代碼

MyLayoutonInterceptTouchEvent()返回false時,分別點擊空白處、點擊按鈕,log日誌以下:

//點擊空白處
MyLayout_ViewGroupDispatch: dispatchTouchEvent: 
MyLayout_ViewGroupDispatch: onInterceptTouchEvent: 
MyLayout_ViewGroupDispatch: onTouchEvent: 
//點擊按鈕
MyLayout_ViewGroupDispatch: onInterceptTouchEvent: 
MyLayout_ViewGroupDispatch: dispatchTouchEvent: 
MyLayout_ViewGroupDispatch: onInterceptTouchEvent: 
MyLayout_ViewGroupDispatch: dispatchTouchEvent: 
MyLayout_ViewGroupDispatch: onInterceptTouchEvent: 
MyLayout_ViewGroupDispatch: dispatchTouchEvent: 
MyLayout_ViewGroupDispatch: onInterceptTouchEvent: 
MyLayout_ViewGroupDispatch: dispatchTouchEvent: 
MyLayout_ViewGroupDispatch: onInterceptTouchEvent: 
Activity_ViewGroupDispatch: onClick: 點擊了按鈕1
複製代碼

能夠看到當ViewGroup(MyLayout)onInterceptTouchEvent()返回true時,並無觸發按鈕的點擊事件,而且自身的onTouchEvent()方法被調用,當返回false時,按鈕的點擊事件觸發,但自身的onTouchEvent()方法未被調用。

並且在默認狀態下,onInterceptTouchEvent()必定會被調用。

以上現象是什麼緣由呢?接下來咱們看看ViewGroup的dispatchTouchEvent()方法的源碼

7.2 源碼解析

目的:

一、研究當ViewGroup(MyLayout)onInterceptTouchEvent()返回true時,並無觸發按鈕的點擊事件,而且自身的onTouchEvent()方法被調用,當返回false時,按鈕的點擊事件觸發,但自身的onTouchEvent()方法未被調用的緣由

二、分發事件是怎麼從ViewGroup分發到View中的

ViewGroupdispatchTouchEvent()源碼:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
	...
        
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
  		......
         //一大堆代碼 
    }
    
    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}
複製代碼

咱們想要了解事件是如何分發,實際上是主要看ViewGroupdispatchTouchEvent()方法何時返回true,何時返回false。看源碼能夠知道,ViewGroupdispatchTouchEvent()方法返回的是handle的值,因此咱們只須要觀察該方法內改變handle值的語句。

首先初始化了handle的值,默認爲false

而後你能夠看到dispatchTouchEvent()的大部份內容都在if (onFilterTouchEventForSecurity(ev)) {}這個條件判斷內,也就是說若是onFilterTouchEventForSecurity(ev)方法返回true的話,那麼就進入該if判斷內。若返回false,則dispatchTouchEvent()返回初始值爲falsehandled,表示不分發事件。

查看下onFilterTouchEventForSecurity(ev)方法

public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    //noinspection RedundantIfStatement
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
        && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        // Window is obscured, drop this touch.
        return false;
    }
    return true;
}
複製代碼
  • FILTER_TOUCHES_WHEN_OBSCUREDandroid:filterTouchesWhenObscured屬性所對應的。android:filterTouchesWhenObscuredtrue的話,則表示其餘視圖在該視圖之上,致使該視圖被隱藏時,該視圖就再也不響應觸摸事件。
  • MotionEvent.FLAG_WINDOW_IS_OBSCUREDtrue的話,則表示該視圖的窗口是被隱藏的

而咱們並無在XML中爲控件設置android:filterTouchesWhenObscured屬性,因此它==0,沒有進入if()方法,因此onFilterTouchEventForSecurity()方法返回true,那麼if (onFilterTouchEventForSecurity(ev))判斷一定會進入以下的判斷中。

接下來咱們看看if(onFilterTouchEventForSecurity(ev))判斷下的內容

if (onFilterTouchEventForSecurity(ev)) {
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;
    
    if (actionMasked == MotionEvent.ACTION_DOWN) {        
        cancelAndClearTouchTargets(ev);
        
        resetTouchState();       
    }   
    
 	......   
}
複製代碼
private void resetTouchState() {
    
    clearTouchTargets();
    
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}
複製代碼
private void clearTouchTargets() {
    TouchTarget target = mFirstTouchTarget;
    if (target != null) {
        do {
            TouchTarget next = target.next;
            target.recycle();
            target = next;
        } while (target != null);
        mFirstTouchTarget = null;
    }
}
複製代碼

能夠看到,若爲ACTION_DOWN事件,就會觸發 cancelAndClearTouchTargets(ev)resetTouchState()方法,在resetTouchState()方法中,有一個clearTouchTargets()方法,而在 clearTouchTargets()方法內會將mFirstTouchTarget設置爲null。咱們暫時先記住這個mFristTouchTarget已經置爲null了。

咱們再看if (onFilterTouchEventForSecurity(ev)){}該判斷內的其餘代碼

if (onFilterTouchEventForSecurity(ev)) {
    ......
    //記錄是否攔截 
    final boolean intercepted;
    
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        
        //判斷是否設置了FLAG_DISALLOW_INTERCEPT這個標記位,默認爲false
        //disallowIntercept表明禁止攔截判斷
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        
        if (!disallowIntercept) {
            //關鍵看這裏
            intercepted = onInterceptTouchEvent(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;
    }     
    ......
}
複製代碼

前面咱們知道了mFirstTouchTargetnull,因此說只要是ACTION_DOWN事件,就會進入到if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {}該方法塊內,由於咱們沒有設置FLAG_DISALLOW_INTERCEPT屬性,因此它爲默認爲false,因此進入到了if (!disallowIntercept) {}方法塊內,調用了onInterceptTouchEvent()方法。這裏就解釋了爲什麼默認狀況下dispatchTouchEvent()後會調用onInterceptTouchEvent()方法。

接下來咱們看下if (onFilterTouchEventForSecurity(ev)) {}方法塊其他部分源碼

if (onFilterTouchEventForSecurity(ev)) {
    ......
        if (!canceled && !intercepted) {
        final View[] children = mChildren;
            
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);
            
       		//判斷子元素是否可以接受點擊事件
            if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }
            
            newTouchTarget = getTouchTarget(child);
            if (newTouchTarget != null) {                      
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }
            
            resetCancelNextUpFlag(child);
            //調用子元素的dispatchTouchEvent方法
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                // Child wants to receive touch within its bounds.
                mLastTouchDownTime = ev.getDownTime();
                if (preorderedList != null) {
                // childIndex points into presorted list, find original index
                    for (int j = 0; j < childrenCount; j++) {
                        if (children[childIndex] == mChildren[j]) {
                            mLastTouchDownIndex = j;
                            break;
                        }
                    }
                } else {
                    mLastTouchDownIndex = childIndex;
                }
                mLastTouchDownX = ev.getX();
                mLastTouchDownY = ev.getY();
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }                           
    }            
}
複製代碼

在上一個方法塊中,intercepted=onInterceptTouchEvent()的返回值,當不攔截的時候,intercepted==false,進入到if (!canceled && !intercepted) {}方法塊內。能夠看到咱們經過for循環遍歷了全部的子元素,而後判斷子元素是否可以接收到點擊事件。判斷子元素是否可以接收點擊事件由兩點決定:一、canViewReceivePointerEvents(child)判斷點擊事件座標是否落在子元素的區域內,二、isTransformedTouchPointInView(x, y, child, null)判斷子元素是否在播放動畫。

因此說當onInterceptTouchEvent()返回false時,觸發了點擊事件,返回true時沒有觸發。

若知足這兩個條件,則事件傳遞給它處理。有一個爲ture則不會進入到該if (!canceled && !intercepted) {}方法塊內,而是執行下面的代碼。

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {}這個方法塊,dispatchTransformedTouchEvent()實際上是調用子元素的dispatchTouchEvent()方法,dispatchTransformedTouchEvent()源碼以下:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
    
    ......
    if (child == null) {
        //child爲null,調用父類的dispatchTouchEvent方法,ViewGroup父類爲View,因此是調用View的dispatchTouchEvent方法。
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
        
       	//child不爲null 
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    ......

}
複製代碼

能夠看到當child不爲null時,調用child.dispatchTouchEvent(transformedEvent),完成了從ViewGroupView的事件分發。

7.3 事件怎麼從ViewGroup分發到View中的

在上面的源碼中,ViewGroupdispatchTouchEvent方法內,當onInterceptTouchEvent返回false時,會調用dispatchTransformedTouchEvent()方法,而該方法內會調用ViewdispatchTouchEvent,在這裏實現了事件從ViewGroupView的事件分發。

7.4 小結:ViewGroup分發的流程圖

8. View的事件分發

8.1 Demo演示

(1) 自定義一個MyButton,繼承自Button,重寫dispatchTouchEvent()onTouchEvent()方法並打印日誌

public class MyButton extends AppCompatButton {
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {        
        Log.i(TAG, "dispatchTouchEvent: ");        
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "onTouchEvent: ACTION_DOWN");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }       
}
複製代碼

(2)Activity佈局中,放入該控件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".viewdiapatch.ViewDispatchActivity">

    <com.ld.eventdispatchdemo.viewdiapatch.MyButton   
        android:id="@+id/btn_click"
        android:text="view的點擊事件分發"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clickable="true"/>
</LinearLayout>
複製代碼

(3)Activity內爲按鈕添加點擊事件和touch事件

public class ViewDispatchActivity extends AppCompatActivity {
    
    private static final String TAG = "Activity_viewDispatch";
    private Button btnClick;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        btnClick = findViewById(R.id.btn_click);
        btnClick.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick: ");                
            }
        });
        
        btnClick.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.i(TAG, "onTouch: ");
                        break;
                }
                //這裏暫時先返回false查看日誌
                return false; // 返回false,onTouchEvent會被調用
            }
        });
    }    
}
複製代碼

當按鈕的onTouch()方法的返回值爲false時,打印的log日誌爲:

MyButton_viewDispatch: dispatchTouchEvent: 
Activity_viewDispatch: onTouch: 
MyButton_viewDispatch: onTouchEvent: ACTION_DOWN
MyButton_viewDispatch: dispatchTouchEvent: 
MyButton_viewDispatch: dispatchTouchEvent: 
MyButton_viewDispatch: dispatchTouchEvent: 
MyButton_viewDispatch: dispatchTouchEvent: 
MyButton_viewDispatch: dispatchTouchEvent: 
Activity_viewDispatch: onClick: 
複製代碼

當按鈕的onTouch()方法的返回值爲true時,打印的log日誌爲:

MyButton_viewDispatch: dispatchTouchEvent: 
Activity_viewDispatch: onTouch: 
MyButton_viewDispatch: dispatchTouchEvent: 
MyButton_viewDispatch: dispatchTouchEvent: 
MyButton_viewDispatch: dispatchTouchEvent: 
MyButton_viewDispatch: dispatchTouchEvent: 
MyButton_viewDispatch: dispatchTouchEvent: 
複製代碼

能夠看到當ViewonTouch()方法返回false時,ViewonTouchEvent()方法和onClick()方法會被調用,當返回true時,這兩個方法都不會被調用。這是什麼緣由呢?

8.2 源碼解析

目的:

一、想得知爲什麼ViewonTouch()返回false時,它的onTouchEvent()onClick()方法會被調用,而返回false時都不會被調用。

咱們看ViewdispatchTouchEvent()方法源碼

public boolean dispatchTouchEvent(MotionEvent event) {
	......
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ......
}
複製代碼

(mViewFlags & ENABLED_MASK) == ENABLED表明控件enableli.mOnTouchListener表明其設置的OnTouchListener,當咱們爲View經過setOnTouchListener()方法設置touch監聽事件時,li.mOnTouchListener就不爲空。li.mOnTouchListener.onTouch(this, event)表明onTouch()方法的返回值。

因此說當咱們設置了onTouch監聽事件並返回false時,源碼這裏的result=false,因此if(!result&&onTouchEvent(event))內的onTouchEvent方法會被調用。

onTouch()返回true時,if(!result&&onTouchEvent(event))內!result==false,因此後面的onTouchEvent()方法不會被調用。

onTouchEvent()不被調用的時候,onClick()也不會被調用,他倆可能有關係,咱們看下onTouchEvent()的源碼

public boolean onTouchEvent(MotionEvent event) {
    
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
		|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
		|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return clickable;
    }
    
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                ......
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {  
                    ......
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        removeLongPressCallback();
                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }                    
                    }                               
			break;        
		return true;
	}        
    return false;            
}
複製代碼

由其中的

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
	|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
	|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
複製代碼

能夠看到,只要Viewclickablelongclickable有一個爲true,那麼clickable就會爲true。而後會進入到switch語句中,在通過各類判斷後會執行到performClickInternal()方法,而該方法源碼爲如下內容

private boolean performClickInternal() {    
    notifyAutofillManagerOnClick();
    return performClick();
}
複製代碼

能夠看到調用了performClick()方法,接下來看它的源碼

public boolean performClick() {
       
    notifyAutofillManagerOnClick();    
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);   
    notifyEnterOrExitForAutoFillIfNeeded(true);    
    return result;
}
複製代碼

能夠看到li.mOnClickListener.onClick(this);,調用了click方法,因此說onCLick()方法在onTouchEvent()方法內被調用,onTouchEvent不被執行,那麼onCLick必定不會執行。

8.3 小結:View分發的流程圖

9. 一個U形圖解釋

10. 總結

其實若是隻是想邏輯的話也很好理解。

dispatchTouchEvent表明分發事件,onInterceptTouchEvent()表明攔截事件,onTouchEvent()表明消耗事件,由本身處理。

默認狀態下事件是按照從ActivityViewGroup再到View的順序進行分發的,分發下去處不處理是另外一回事,分發完成後,不處理則向上一層回調,調用上一層的onTouchEvent進行處理事件,若onTouchEvent返回true,則表示在該層消耗了事件,若返回false,表示事件還沒被處理,須要再向上回調一層,調用上一層的onTouchEvent方法。

以上就是所有內容,第一次分析源碼,有錯誤的地方還望指出,參考文章大神寫得也很詳細,必定要看看。

多說一句,看源碼確實很頭大,尤爲在看不懂卻還要看到這麼多文字就更耐不下心來了,可是當我看到一篇文章下的一個評論時,確實是激勵了我,不懂就多讀,沒有解決不了的問題!

11. 參考文章

Android事件分發——ViewGroup篇

Android事件分發機制徹底解析,帶你從源碼的角度完全理解(下)

Android事件分發機制詳解:史上最全面、最易懂

圖解 Android 事件分發機制

相關文章
相關標籤/搜索