天天看點

Android事件分發全面解析(基礎篇)-夯實基礎

這是一個老生常談的話題了,也是一個初級開發者必須掌握的技能,但有多少人真正明白呢,夯實基礎,方能長遠。

本文不過多涉及基礎性問題,建議有點了解再來閱讀,或者直接硬幹。
Android事件分發全面解析(基礎篇)-夯實基礎

首先,事件分發對象是誰?

事件。

當使用者觸摸螢幕時( View 或 ViewGroup 派生的控件),将産生點選事件(Touch事件)

Touch事件相關細節,比如觸摸位置,時間,手勢等等,會被封裝成 MotionEvent 對象。

Touch 事件主要有以下幾種:

事件 簡介
ACTION_DOWN 手指 初次接觸到螢幕 時觸發。
ACTION_MOVE 手指 在螢幕上滑動 時觸發,會會多次觸發。
ACTION_UP 手指 離開螢幕 時觸發。
ACTION_CANCEL 事件 被上層攔截 時觸發。

事件列:從手指接觸螢幕至手指離開螢幕,這個過程産生一系列時間,任何時間都是以Down事件開始,UP事件結束,中間會有無數Move事件。

[外鍊圖檔轉存失敗(img-CBqGwtSj-1562061405691)(…/…/assets/944365-79b1e86793514e99.png)]

也就是說,當一個 MotionEvent 産生後,系統需要把這個事件傳遞給一個具體View去處理。

什麼是事件分發?

要知道什麼是事件分發,其實也就是對MotionEvent 事件的分發過程,也就是當一個 手指 按下之後,系統需要把這個事件傳遞給一個具體的 View,而這個傳遞的過程就是分發過程。

分發順序也就是 Activity(Window)-> ViewGroup -> View.

三大方法: - >(入門)

Android事件分發全面解析(基礎篇)-夯實基礎

說到分發過程,就不得不說這三個方法了:

  1. dispatchTouchEvent,
  2. onInterceptTouchEvent
  3. onTouchEvent

先介紹一下這三個方法分别是幹啥的:

dispatchTouchEvent

用來進行事件的分發,如果事件能夠傳遞給目前View,那麼此方法一定會被調用,傳回結果受目前View 的 onTouchEvent 和 下級View的 dispatchTouchEvent 方法的影響,表示是否消耗目前事件。

onInterceptTouchEvent

在 dispatchTouchEvent内部調用,用來判斷是否攔截某個事件,如果目前View攔截了某個事件,那麼在同一個事件序列中,此方法不會被再次調用,傳回結果表示是否攔截目前事件。

onTouchEvent

在 'dispatchTouchEvent '方法中調用,用來處理點選事件,傳回結果表示是否消耗目前事件,如果不消耗,則在同一個事件序列中,目前View 無法再次接受到事件。

用一個圖來解釋

表示有該方法。

X

表示沒有該方法。
類型 相關方法 ViewGroup View
事件分發 dispatchTouchEvent
事件攔截 onInterceptTouchEvent X
事件消費 onTouchEvent

看到這,不知道你們有沒有這個疑問,為啥View會有 dispatchTouchEvent 方法?

一個View 可以注冊很多監聽器吧,例如單擊,長按,觸摸事件(onTouch),并且View 本身也有 onTouchEvent 方法,那麼問題來了,這麼多事件相關的方法應該由誰管理,是以View也會有這個 方法。

他們之間的關系我們可以通過以下的僞代碼來看:

// 點選事件産生後,會直接調用dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {

    //代表是否消耗事件
    boolean consume = false;


    if (onInterceptTouchEvent(ev)) {
    //如果onInterceptTouchEvent()傳回true則代表目前View攔截了點選事件
    //則該點選事件則會交給目前View進行處理
    //即調用onTouchEvent ()方法去處理點選事件
      consume = onTouchEvent (ev) ;

    } else {
      //如果onInterceptTouchEvent()傳回false則代表目前View不攔截點選事件
      //則該點選事件則會繼續傳遞給它的子元素
      //子元素的dispatchTouchEvent()就會被調用,重複上述過程
      //直到點選事件被最終處理為止
      consume = child.dispatchTouchEvent (ev) ;
    }

    return consume;
   }
           

通過上面的僞代碼我們可以發現,對于一個根 ViewGroup 來說,點選事件産生後,首先會傳遞給它,這時他的 dispatchTouEvent 就會調用,如果這個 ViewGroup 的 onInterceptTouchEvent方法傳回true,就表示它要攔截目前事件,接着事件就會交給這個 ViewGroup 處理,即它的 onTouchEvent 方法會被調用,如果這個 ViewGroup 的onInterceptTouchEvent 方法傳回false 就表示它不攔截目前事件,這時目前事件就會繼續傳遞給它的 子元素,接着子元素 dispatchTouEvent 方法就會被調用。如此反複知道事件最終被處理。

用一張搬運過來的事件分發流程圖來說明一下:

Android事件分發全面解析(基礎篇)-夯實基礎

當一個View需要處理事件時,如果它設定了 OnTouchListener, 那麼 OnTouchListener 中的 onTouch 方法會被回調。這時事件如何處理還要看 onTouch 的傳回值,如果傳回false,則目前View 的onTouchEvent 方法會被調用;如果傳回 true,那麼 onTouchEvent 方法将不會被調用。由此可見,給View設定 onTouchListener,其優先級比 onTouchEvent 還要高。在 onTouchEvent 方法中,如果目前設定有 onClickListener,那麼它的 onClick 方法會被調用。可以看出,平時我們常用的 onClickListener,其優先度最低,即處于事件傳遞的尾端.

通過以下源碼即可看出:

public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        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;
        }
        ......
        return result;
}
           

關于事件傳遞機制,我們可以總結出以下結論,根據這些結論能更好的了解整個傳遞機制:(摘錄自Android開發藝術探索)

  1. 同一個事件序列是指從手指接觸螢幕的那一刻起,到手指離開螢幕的那一刻結束,在這個過程中所産生的的一系列時間,這個事件序列以 down 事件開始,中間含有數量不定的 move 事件,最終以 up 事件結束。
  2. 正常情況下,一個事件序列隻能被一個 View 攔截且消耗。這一條原因可以參考(3),因為一旦一個元素攔截了某次事件,那麼同一個事件序列内的所有事件都會直接交給它處理,是以同一個事件序列中的事件不能分别由兩個 View同時處理,但是通過特殊手段可以做到,比如一個 View 将本該自己處理的事件 通過 onTouchEvent 強行傳遞給其他View處理。
  3. 某個View一旦決定攔截,那麼這一個事件序列都隻能由它來處理(如果事件序列能夠傳遞給它的話),并且它的 onInterceptTouchEvent 不會再被調用。這條也很好了解,就是說當一個 View 決定攔截一個事件後,那麼系統會把同一個事件序列内的其他方法都直接交給它來處理,是以就不再調用這個View的 onInterceptTouchEvent 去詢問它是否要攔截了。
  4. 某個View 一旦開始處理事件,如果它不消耗 ACTION_DOWN 事件( onTOuchEvent 傳回了 false ) ,那麼同一事件序列中的其他事件都不會交給 它來處理,并且事件将重新交由它的父元素去處理,即父元素的 onTouchEvent 會被調用。意思就是事件一旦交給一個 View 處理,那麼它就必須消耗掉,否則同一事件序列中剩下的事件就不再交給它來處理了,這就好比上級交給程式猿一件事,如果事情沒有處理好,短期内上級就不敢再把事情交給程式猿去做了。二者道理差不多。
  5. 如果View 不消耗除 ACTION_DOWN 以外的其他事件,那麼這個點選事件會消失,此時父元素的 onTouchEvent 并不會被調用,并且目前 View 可以持續收到後續的事件,最終這些消失的點選事件會傳遞給 Activity 處理。
  6. ViewGroup 預設不攔截任何事件。Android 源碼中ViewGroup 的 onInterceptTouchEvent 方法預設傳回false.
  7. View 沒有 onInterceptTouchEvent 方法,一旦有點選事件傳遞給它,那麼它的 onTouchEvent 方法就會被調用。
  8. View 的 onTouchEvent 預設都會消耗事件(傳回true),除非它是不可點選的(clickable和longClickable同時為false).View 的 longClickable 屬性預設為 false,clickable 屬性要分情況,比如Button 的clickable 屬性預設為true,而 TextView 的 clickable 屬性預設為 false.
  9. View 的 enable 屬性不受影響 onTouchEvent 的 預設傳回值,哪怕一個 View 是 disable 狀态的,隻要它的 clickable 或者 longClickable 有一個 為 true,那麼它的 onTouchEvent 就傳回 true.
  10. onClick 會發生的前提是目前View 是可點選的,并且它收到了 down 和 up 的事件。
  11. 事件傳遞過程是由外而向的,即事件總是先傳遞給父元素,然後再由父元素分發給子View,通過requestDisallowInterceptTouchEvent 方法可以在子元素中幹預父元素的事件分發過程,但是 ACTION_DOWN 事件除外。

執行個體: ->(實踐)

Android事件分發全面解析(基礎篇)-夯實基礎

結合上面的結論,我們來用執行個體來示範一下

首先用這樣一個圖來看

Android事件分發全面解析(基礎篇)-夯實基礎

這是一個簡單的布局,Activity裡面一個LinearLayout,用來代替ViewGroup,内部是一個Button,沒什麼說的。

我們要從如下幾個方面入手,探究上面那些結論。

  1. 預設情況下的事件分發
  2. 攔截時的分發情況
  3. 如果viewagroup 不攔截 Down事件,即還是 Button 來處理,但它攔截接下來的move事件(也就是半路攔截),那麼接下來情況又會是咋樣?
  4. 如果全部都不消費事件,事件最終由誰來安排。
  5. onTouch中傳回 true或者false,對onTouchEvent有什麼影響嗎。

上代碼啦:

首先繼承自 LinearLayout,為了重寫相應的 三大方法。

/**
 * @author Petterp on 2019/7/2
 * Summary:重寫LinearLayout相應方法
 * 郵箱:[email protected]
 */
public class LinearLayoutView extends LinearLayout {
    public LinearLayoutView(Context context) {
        super(context);
    }

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

    /**
     * 事件分發
     *
     * @param ev
     * @return 是否消費目前事件
     * true-> 消費事件,後續事件繼續分發到該view
     * false->
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 事件攔截,即自己處理事件
     *
     * @param ev
     * @return 是否攔截目前事件
     * true->事件停止傳遞,執行自己onTouchEvent,該方法後續不再調用
     * false -> 事件繼續向下傳遞,調用子view.dispatchTouchEvent(),該方法後續仍被調用
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e("demo", "viewgroup-onInterceptTouchEvent: "+ ViewActivity.mode);
        return ViewActivity.mode;
    }

    /**
     * 處理點選事件
     *
     * @param event
     * @return 是否消費目前事件
     * true-> 事件停止傳遞,後續事件由其處理
     * false-> 不處理,事件交由父 onTouchEvent()處理,此view不再接受此事件列的其他事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
}
           

接着是Activity:

public class ViewActivity extends AppCompatActivity {
    //mode标記位
    public static boolean mode=false;


    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view);
        Button btn_1=findViewById(R.id.btn_t1);
        LinearLayoutView linearLayout=findViewById(R.id.ln_group);

        //LinearLayout 即viewGroup
        linearLayout.setOnClickListener(v -> Log.e("demo","viewgroup-onClick"));
        linearLayout.setOnTouchListener((v, event) -> {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.e("demo","viewgroup-手指按下");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("demo","viewgroup-手指放開");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e("demo","viewgroup-手指移動");
                    break;
            }
            return false;
        });



        //button 即子view
        btn_1.setOnClickListener(v -> Log.e("view-demo","onClick"));
        btn_1.setOnTouchListener((v, event) -> {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.e("demo","view-手指按下");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("demo","view-手指放開");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e("demo","view-手指移動");
                    break;
                case MotionEvent.ACTION_CANCEL:
                    Log.e("demo","view-被父view截斷了");
                    break;
            }
            return false;
        });
    }

    /**
     * 重寫Activity onTouchEvent,
     * 模拟子view不消費事件時的處理情況
     * @param event 
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("demo","Activity-自己消化");
        return super.onTouchEvent(event);
    }
}
           

相應的注釋簡潔明了,不用多說了吧。

xml

<?xml version="1.0" encoding="utf-8"?>
<com.petterp.studybook.View.LinearLayoutView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/ln_group"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".View.ViewActivity">

    <Button
        android:id="@+id/btn_t1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        tools:ignore="HardcodedText" />
</com.petterp.studybook.View.LinearLayoutView>
           
Android事件分發全面解析(基礎篇)-夯實基礎

首先看看預設情況下的事件分發過程:

手指-> 點選Button,再點選空白處(即Viewgroup)。觀察日志列印:

Android事件分發全面解析(基礎篇)-夯實基礎

結論:預設情況下,viewgroup 攔截器傳回false,事件會傳遞到子view的 dispatchTouchEvent 然後繼續分發,直到最後被 子view 的onTouchEvent 所消費,這時候會調用 onclick方法,是以 onclick處于優先級最低。

攔截情況下的事件分發,比如,ViewGroup 的onInterceptTouchEvent傳回true呢?

更改代碼

//mode标記位
public static boolean mode=true;
           

預設這裡是false,當然這個是我自己寫的一個标記位,真實情況肯定不是這樣。隻是為了模拟效果。現在改為true,這樣的話LinearLayout 的onInterceptTouchEvent 将傳回true,也就是ViewGroup 消費了此事件。

手指 -> 點選Button,再點選 空白處(即ViewGroup),觀察日志列印:

Android事件分發全面解析(基礎篇)-夯實基礎

可以發現,viewGroup已經消費了此次事件,這時無論點選什麼位置,事件都不會傳遞到子view。

結論:當 dispatchTouchEvent 攔截了此次事件,那麼接下來的事件序列都不會向下傳遞。都由此view處理。

如果我們先不攔截,當點選之後再攔截子view事件,這時候又會是什麼情況?

修改代碼:

//mode标記位
public static boolean mode=false;

...
btn_1.setOnTouchListener((v, event) -> {
      switch (event.getAction()){
          case MotionEvent.ACTION_DOWN:
              Log.e("demo","view-手指按下");
              mode=true;
              break;
        ...
      }
      return false;
  });
           

手指 -> button,然後稍微移動一下松開:

Android事件分發全面解析(基礎篇)-夯實基礎

是不是發現此次事件序列在被攔截時傳遞了一個 ACTION_CANCEL 給子view,而以後後續事件都不會再向下傳遞。

結論:當一個事件 被 onInterceptTouchEvent 傳回true 中途攔截時,會傳遞 ACTION_CANCEL 給view的 onTouchEvent方法。後續的 move等事件,會直接傳遞給 攔截者 的 onTouchEvent( ) 方法。而且後續事件不會在傳遞給 其 onInterceptTouchEvent 方法,該方法一旦傳回一次true,就再也不會被調用了。

如果都不處理,消費此次事件,會是怎樣的呢?

修改代碼:

這裡為了示範,因為預設的 onInterceptTouchEvent 傳回true,懶得修改Button,是以我們嘗試用ViewGroup攔截事件,然後不消費它,也就是 onTOuchEvent 傳回false。

//mode标記位
public static boolean mode=false;

LinearLayoutView ->更改代碼
  public boolean onTouchEvent(MotionEvent event) {
        Log.e("demo","viewgroup-我不消費");
        return false;
    }
           

手指-> button 按下輕輕移動:

Android事件分發全面解析(基礎篇)-夯實基礎

結論:這也就是我們常說的責任鍊模式,層層傳遞事件,決定分發dispatchTouchEvent,最終由onTouchEvent接收,如果子不消費,就繼續向上,直到Activity自己消費。Activity這裡,其實無論傳回true還是false,都會消費事件。

onTouch中傳回 true或者false,對onTouchEvent有什麼影響嗎?

先将代碼恢複如初,然後更改

Activity: 
linearLayout.setOnTouchListener((v, event) -> {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.e("demo","viewgroup-手指按下");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("demo","viewgroup-手指放開");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e("demo","viewgroup-手指移動");
                    break;
            }
            return true;
        });

LinearLayout:
  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean mode=super.onInterceptTouchEvent(ev);
        Log.e("demo", "viewgroup-onInterceptTouchEvent: "+ mode);
        return false;
    }


 @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean mode=super.onTouchEvent(event);
        Log.e("demo","viewgroup-onTouchEvent");
        return mode;
    }
           

手指-> 按下空白區域再松開:

Android事件分發全面解析(基礎篇)-夯實基礎

接着修改代碼

Activity: 
 linearLayout.setOnTouchListener((v, event) -> {
         	...
            return false;
        });
           

手指-> 按下空白區域再松開:

Android事件分發全面解析(基礎篇)-夯實基礎

結論:可以發現 onTouch方法優先于 onTouchEvent執行。具體的原因可以看我下一篇Android事件分發全面解析(源碼篇)

更多Android開發知識請通路—— Android開發日常筆記,歡迎Star,你的小小點贊,是對我的莫大鼓勵。

參閱:

  • GcsSloop
  • Android開發藝術探索

繼續閱讀