Android事件傳遞分析-傳遞日志分析

前言

事件傳遞在android中是最基礎的知識,但也是最繁瑣復雜的知識,在5.0后傳遞變的更加復雜,網(wǎng)上有很多文章分析事件分發(fā),但是一開始就是在源碼里面進行各種講解這讓整個事件傳遞消費事件的宏觀性不強,本文章從最基礎的日志開始分析傳遞事件,如果有耐心把文章認真分析完,我相信事件傳遞你已經(jīng)了解了一半了!后面再配合源碼進行理解,相信能做到更好!

方法解釋

  1. dispatchTouchEvent:事件分發(fā)
  2. onInterceptTouchEvent:事件攔截
  3. onTouchEvent:事件處理
  4. 內向傳遞:事件傳遞由外向內傳遞
  5. 外向處理:事件處理由內向外傳遞

注意:viewgroup中包含如上三個方法,但是view只包含其中兩個,沒有onInterceptTouchEvent方法

自定義組件

為了更好的解釋傳遞與消費的過程,我定義了兩個容器兩個view,分別是ViewGroupA,ViewGroupB , viewC ,ViewD,并分別重寫他們的onInterceptTouchEvent,dispatchTouchEvent,onTouchEvent

代碼如下:

public class ViewGroupA extends LinearLayout {
    public ViewGroupA(Context context) {
        super(context);
    }

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

    public ViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = super.onInterceptTouchEvent(ev);
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=" + result);
        return result;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = super.dispatchTouchEvent(ev);
        Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent return super.dispatchTouchEvent(ev)= " + result);
        return result;
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = super.onTouchEvent(ev);
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent return super.onTouchEvent(ev)=" + result);
        return result;
    }

}

public class ViewGroupB extends LinearLayout {
    public ViewGroupB(Context context) {
        super(context);
    }

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

    public ViewGroupB(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = super.onInterceptTouchEvent(ev);
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=" + result);
        return result;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = super.dispatchTouchEvent(ev);
        Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent return super.dispatchTouchEvent(ev)= " + result);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = super.onTouchEvent(ev);
        //boolean result = true;
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent return super.onTouchEvent(ev)=" + result);
        return result;
    }
}


public class ViewC extends View {
    public ViewC(Context context) {
        super(context);
    }

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

    public ViewC(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = super.dispatchTouchEvent(ev);
        Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent return super.dispatchTouchEvent(ev)= " + result);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = super.onTouchEvent(ev);
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent return super.onTouchEvent(ev)=" + result);
        return result;
    }
}

public class ViewD extends View {
    public ViewD(Context context) {
        super(context);
    }

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

    public ViewD(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = super.dispatchTouchEvent(ev);
        Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent return super.dispatchTouchEvent(ev)= " + result);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = super.onTouchEvent(ev);
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent return super.onTouchEvent(ev)=" + result);
        return result;
    }
}

工具方法:

public class ViewUtils {
    public static final int ACTION_DOWN = 0;

    /**
     * Constant for {@link #getActionMasked}: A pressed gesture has finished, the
     * motion contains the final release location as well as any intermediate
     * points since the last down or move event.
     */
    public static final int ACTION_UP = 1;

    /**
     * Constant for {@link #getActionMasked}: A change has happened during a
     * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
     * The motion contains the most recent point, as well as any intermediate
     * points since the last down or move event.
     */
    public static final int ACTION_MOVE = 2;

    /**
     * Constant for {@link #getActionMasked}: The current gesture has been aborted.
     * You will not receive any more points in it.  You should treat this as
     * an up event, but not perform any action that you normally would.
     */
    public static final int ACTION_CANCEL = 3;

    static String actionToString(int i) {
        switch (i) {
            case 0:
                return "ACTION_DOWN";
            case 1:
                return "ACTION_UP";
            case 2:
                return "ACTION_MOVE";
            case 3:
                return "ACTION_CANCEL";
            default:
                return "未知";
        }
    }
}

布局搭建

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <com.remote.activity.weight.ViewGroupA
            android:layout_marginTop="10dp"
            android:id="@+id/viewGroupA"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:background="@color/colorAccent">

        <com.remote.activity.weight.ViewGroupB
                android:id="@+id/viewGroupB"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_margin="60dp"
                android:orientation="vertical"
                android:background="@android:color/holo_blue_dark">

            <com.remote.activity.weight.ViewC
                    android:id="@+id/viewC"
                    android:layout_width="match_parent"
                    android:layout_height="90dp"
                    android:layout_margin="60dp"
                    android:background="@android:color/holo_green_dark"/>
            <com.remote.activity.weight.ViewD
                    android:id="@+id/viewD"
                    android:layout_width="match_parent"
                    android:layout_height="90dp"
                    android:layout_margin="60dp"
                    android:background="@color/colorPrimary"/>
        </com.remote.activity.weight.ViewGroupB>
    </com.remote.activity.weight.ViewGroupA>


</LinearLayout>

圖解:


image.png

日志分析

不手動消耗事件不手動攔截事件的日志

通過上面的布局完成后,我們什么也不做,直接點擊viewC控件日志如下:

 D/點擊事件: ViewGroupA dispatchTouchEvent -> ACTION_DOWN
 D/點擊事件: ViewGroupA onInterceptTouchEvent -> ACTION_DOWN
 D/點擊事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
 D/點擊事件: ViewGroupB dispatchTouchEvent -> ACTION_DOWN
 D/點擊事件: ViewGroupB onInterceptTouchEvent -> ACTION_DOWN
 D/點擊事件: ViewGroupB onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
 D/點擊事件: ViewC dispatchTouchEvent -> ACTION_DOWN
 D/點擊事件: ViewC onTouchEvent -> ACTION_DOWN
 D/點擊事件: ViewC onTouchEvent return super.onTouchEvent(ev)=false
 D/點擊事件: ViewC dispatchTouchEvent return super.dispatchTouchEvent(ev)= false
 D/點擊事件: ViewGroupB onTouchEvent -> ACTION_DOWN
 D/點擊事件: ViewGroupB onTouchEvent return super.onTouchEvent(ev)=false
 D/點擊事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= false
 D/點擊事件: ViewGroupA onTouchEvent -> ACTION_DOWN
 D/點擊事件: ViewGroupA onTouchEvent return super.onTouchEvent(ev)=false
 D/點擊事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= false

點擊viewC后,因為viewC與viewD是在同一個容器中,點擊的坐標區(qū)域并不涉及到viewD,所以日志中并沒有viewD的信息(下文相同)

分析

點擊viewC后:

  1. 事件首先傳遞到了最外層容器viewgroupA中,并且回調了dispatchTouchEvent()方法,然后又回調了onInterceptTouchEvent()方法,因為viewgroupA中還有其他組件,并且我們沒有手動設置返回值,所以這里onInterceptTouchEvent()一定會返回false將事件傳遞下去
  2. 事件這時又傳遞到viewgroupB中,并且回調了dispatchTouchEvent()方法,然后又回調了onInterceptTouchEvent()方法,因為viewgroupB中還有其他組件,并且我們沒有手動設置返回值,所以這里一定onInterceptTouchEvent()會返回false將事件傳遞下去,道理同viewgroupA一樣
  3. 事件又傳遞到了viewC,同樣第一步回調dispatchTouchEvent(),然后回調onTouchEvent(),因為這里沒做消耗事件的處理,所以這里又返回false,因為這里到了最底層的view了,并且沒有處理這個事件,所以這個事件又會通過dispatchTouchEvent返回false,向上傳遞給viewgroupB
  4. 因為viewC沒有消耗事件,所以viewgroupB又接受到這個事件并回調onTouchEvent()來處理,因為viewgroupB也沒有編寫消耗事件的代碼,所以他又通過dispatchTouchEvent返回false,向上傳遞給viewgroupA
  5. 因為viewgroupB沒有消耗事件,所以viewgroupA又接受到這個事件并回調onTouchEvent()來處理,因為viewgroupA也沒有編寫消耗事件的代碼,所以他又通過dispatchTouchEvent返回false,向上傳遞給他的上層,至于后面我們就可以不用分析了,就是傳遞給DecorView,PhoneWindow了!

這里日志沒有涉及到ACTION_MOVE,ACTION_UP的原因是,第一個ACTION_DOWN事件還沒處理,后面的事件是不會進行傳遞的

手動消耗事件不手動攔截事件的日志

我們在代碼中編寫這樣的一句代碼

 viewC.setOnClickListener {
            toast("我是C消費了此事件")
        }

同樣點擊viewC后得到日志如下:

D/點擊事件: ViewGroupA dispatchTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupA onInterceptTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/點擊事件: ViewGroupB dispatchTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupB onInterceptTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupB onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/點擊事件: ViewC dispatchTouchEvent -> ACTION_DOWN
D/點擊事件: ViewC onTouchEvent -> ACTION_DOWN
D/點擊事件: ViewC onTouchEvent return super.onTouchEvent(ev)=true
D/點擊事件: ViewC dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/點擊事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/點擊事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/點擊事件: ViewGroupA dispatchTouchEvent -> ACTION_UP
D/點擊事件: ViewGroupA onInterceptTouchEvent -> ACTION_UP
D/點擊事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/點擊事件: ViewGroupB dispatchTouchEvent -> ACTION_UP
D/點擊事件: ViewGroupB onInterceptTouchEvent -> ACTION_UP
D/點擊事件: ViewGroupB onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/點擊事件: ViewC dispatchTouchEvent -> ACTION_UP
D/點擊事件: ViewC onTouchEvent -> ACTION_UP
D/點擊事件: ViewC onTouchEvent return super.onTouchEvent(ev)=true
D/點擊事件: ViewC dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/點擊事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/點擊事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
分析

這個時候可以看到事件的傳遞也是跟上面那種情況一樣的,唯一不同的地方就是viewC中的onTouchEvent()方法返回的是一個true,這里也很明顯,我們前面加了點擊事件的處理,這里其實就是做了一個事件消耗,并且消耗完了這個事件后dispatchTouchEvent()也返回一個true來通知他的父容器我已經(jīng)消耗了這個事件了,一直回調到最頂層view,因為這里處理了ACTION_DOWN事件,所以會出現(xiàn)ACTION_UP事件也需要處理,處理的方式跟ACTION_DOWN是一樣的!

這里大家應該明白了消耗事件是怎么回事了吧,其實就是我們平時設置的一些點擊事件等!

不手動消耗事件手動攔截事件的日志

我們做一個攔截實驗,我們修改viewgroupB中onInterceptTouchEvent()的代碼,我們手動給他返回一個true:

   @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = true;
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=" + result);
        return true;
    }

同樣點擊viewC后得到日志如下:


D/點擊事件: ViewGroupA dispatchTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupA onInterceptTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/點擊事件: ViewGroupB dispatchTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupB onInterceptTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupB onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=true
D/點擊事件: ViewGroupB onTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupB onTouchEvent return super.onTouchEvent(ev)=false
D/點擊事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= false
D/點擊事件: ViewGroupA onTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupA onTouchEvent return super.onTouchEvent(ev)=false
D/點擊事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= false


分析

從直觀的感受似乎日志里面只有viewgroupA與viewgroupB的日志,是的沒錯

  1. 按照常規(guī)事件傳遞到viewgroupA回調dispatchTouchEvent,然后回調onInterceptTouchEvent,因為他里面還有子組件所以onInterceptTouchEvent肯定返回為false向下傳遞
  2. 接受到viewgroupA傳遞過來的事件,回調dispatchTouchEvent方法,然后回調onInterceptTouchEvent方法,但是這里我們手動設置的返回為true,所以我們viewgroupB必須調用onTouchEventr去消費這個事件,并且返回為true,但是我們在onTouchEventr中并沒有些消費事件的代碼,所以onTouchEvent返回false把事件又傳遞給他上級處理
  3. viewgroupA接受到viewgroupB沒有處理的事件然后調用onTouchEventr去處理,但是也沒有些處理的代碼,所以繼續(xù)回調給上級直到phonewindow,
  4. 因為這個ACTION_DOWN事件沒有處理,所以不會收到ACTION_UP等事件了

手動消耗事件手動攔截事件的日志

我們添加消費事件的代碼:

  viewGroupB.setOnClickListener {
            toast("我是B消費了此事件")
        }

同時攔截事件讓viewGroupB的onInterceptTouchEvent返回為true

public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
        boolean result = true;
        Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=" + result);
        return true;
    }

這個時候我們點擊viewC按鈕日志如下:


D/點擊事件: ViewGroupA dispatchTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupA onInterceptTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/點擊事件: ViewGroupB dispatchTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupB onInterceptTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupB onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=true
D/點擊事件: ViewGroupB onTouchEvent -> ACTION_DOWN
D/點擊事件: ViewGroupB onTouchEvent return super.onTouchEvent(ev)=true
D/點擊事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/點擊事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/點擊事件: ViewGroupA dispatchTouchEvent -> ACTION_UP
D/點擊事件: ViewGroupA onInterceptTouchEvent -> ACTION_UP
D/點擊事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/點擊事件: ViewGroupB dispatchTouchEvent -> ACTION_UP
D/點擊事件: ViewGroupB onTouchEvent -> ACTION_UP
D/點擊事件: ViewGroupB onTouchEvent return super.onTouchEvent(ev)=true
D/點擊事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/點擊事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
分析

到了這里大家應該都能猜到會發(fā)生什么現(xiàn)象了吧

  1. 因為viewgroupB的onInterceptTouchEvent手動返回為true所以攔截了事件,日志中不會出現(xiàn)viewC的信息了
  2. 事件傳遞到viewgroupB中被攔截,所以就要調用他的onTouchEvent方法,由于我們又編寫了消耗事件的代碼,所以這里會消耗掉事件,并且返回true通知上級我們已經(jīng)消耗了事件了
  3. 因為這個ACTION_DOWN事件有處理,所以會收到ACTION_UP等事件了進行下一步的傳遞

總結

如果認真的看完這里的日志,相信對事件的消耗分發(fā)了解了一半了,至于為什么點擊的時候沒得viewD的事,還有很多不明白的地方,我們會下次繼續(xù)用源碼進行分析!

提醒

需要源碼的朋友可以發(fā)送請求到郵箱 imkobedroid@gmail.com 文章與代碼有待改進!希望可以交流

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容