Android中事件傳遞機制的總結


 

事件傳遞雖然算不上某個單獨的知識點,但是在實際項目開發中肯定會碰到,如果不明白其中的原理,那在設計各種滑動效果時就會感到很困惑。

關於事件的傳遞,我們可能會有以下疑問

事件是如何傳遞的

事件是如何處理的

自定義view的時候,事件也沖突了怎么解決

帶着這三個疑問,我們來總結一下事件傳遞機制是怎么回事。

 

一、事件分發的原理:

1、事件是如何傳遞的:

(1)首先由Activity分發,分發給根View,也就是DecorView(DecorView為整個Window界面的最頂層View)

(2)然后由根View分發到子的View

如下圖所示:

a981bf33-fe66-4914-9558-4751c474b19f

再來看下面這張圖:(這張圖是整個事件傳遞機制的核心

68067bcc-3d67-4eaf-98db-8906dcd4b521

上圖顯示:

  在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截。onInterceptTouchEvent方法:

    返回true代表不允許事件繼續向子View傳遞,將會觸發當前View的onTouchEvent(),進行事件的消費;

    返回false代表不對事件進行攔截,事件可以傳遞給孩子

    默認返回false

 

2、事件是如何處理的:

86ff9936-f642-4bc5-ac26-d1e2f0ba8c67

再來看下面這張圖:

ba0b3001-afde-4e13-8819-d735293b2526

上圖顯示:子View中如果將傳遞的事件消費掉,父類的ViewGroup中將無法接收到任何事件

 

二、onTouch和onClick事件同時發生的問題:

首先這里要解釋一下各種概念,避免混淆。

1、各種概念:

事件:

  混合體(可能是點擊事件也可能是觸摸事件)。

觸摸事件:

  按下、滑動和離開

點擊事件:

  按下、停留一會兒和離開

觸摸onTouch事件和點擊onClick事件有什么關系?

(1)執行先后不一樣。觸摸事件先執行

(2)觸摸事件返回值影響點擊事件(前者影響后者,而后者不影響前者)

 

2、onTouch和onClick事件同時執行

    如果按鈕的onTouch和onClick方法同時執行,會有什么效果呢?我們通過代碼來看一下:

 1 import android.app.Activity;
 2 import android.os.Bundle;
 3 import android.util.Log;
 4 import android.view.MotionEvent;
 5 import android.view.View;
 6 import android.widget.Button;
 7 
 8 public class MainActivity extends Activity {
 9 
10     private static final String TAG = "MainActivity";
11     private Button btn;
12 
13     @Override
14     protected void onCreate(Bundle savedInstanceState) {
15         super.onCreate(savedInstanceState);
16         setContentView(R.layout.activity_main);
17         btn = (Button) findViewById(R.id.btn);
18 
19         //按鈕的touch觸摸事件
20         btn.setOnTouchListener(new View.OnTouchListener() {
21             @Override
22             public boolean onTouch(View v, MotionEvent event) {
23                 switch (event.getAction()) {
24                     case MotionEvent.ACTION_DOWN: //按下的動作
25                         Log.d(TAG, "btn is MotionEvent.ACTION_DOWN");
26                         break;
27                     case MotionEvent.ACTION_MOVE: //滑動的動作
28                         Log.d(TAG, "btn is MotionEvent.ACTION_MOVE");
29                         break;
30                     case MotionEvent.ACTION_UP: //離開的動作
31                         Log.d(TAG, "btn is MotionEvent.ACTION_UP");
32                         break;
33                 }
34 
35                 return false;  //默認的返回值
36             }
37         });
38 
39         //按鈕的點擊事件
40         btn.setOnClickListener(new View.OnClickListener() {
41             @Override
42             public void onClick(View v) {
43                 Log.d(TAG, "btn is click");
44             }
45         });
46     }
47 
48 }

 

上方代碼中,按鈕btn既包含了onTouch事件,也包含了onClick事件,現在運行程序,點擊按鈕,后台打印的日志如下:

8a294e91-95fd-40e0-ba7a-c04a4cc0e0c3

通過上方日志我們可以看到,onTouch事件是比onClick事件先執行的。

備注:這里提示一下,如果我們僅僅只是用手指點擊按鈕,然后馬上松開,onTouch事件中只會執行ACTION_DOWN和ACTION_UP動作;如果用手機點擊按鈕,並且手指還在按鈕上滑動了一會兒,那么滑動的過程中,ACTION_MOVE動作就會不停的執行。現在我們應該能明白這三個動作的含義了吧?

 

3、只執行onTouch事件,不執行onClick事件:

如果按鈕的onTouch和onClick方法同時執行,在有些情況下不太滿足產品的需求。那如果只想執行onTouch事件,不執行onClick事件,該怎么做呢?很簡單,只需要在上方代碼中,將第39行的代碼改為return true,就行了,即:將onTouch方法的返回值改為true,就會只執行onTouch事件,不執行onClick事件。改完代碼之后,后台的運行效果如下:

3387cba5-3e8d-4eba-afae-efdd8a881491

為什么這樣改代碼就可以了呢?這就需要從源碼的角度來理解了。

button按鈕中沒有dispatchTouchEvent方法,需要去它的父類View.java中去找dispatchTouchEvent方法。源碼如下所示:

ccd3f0f2-1fcc-4abf-9f17-bf41a8c58b6c

上圖分析:紅框部分的onTouch()方法默認是返回false,所以就會執行藍框部分的代碼,即:調用onTouchEvent()方法。而onTouchEvent方法中,會在ACTION_UP動作里面會去初始化onClick事件。如下圖所示:

a8bef793-7950-4dcb-854a-9bedd1af06e0

407110d1-630d-4d88-a294-85231d0ad447

於是onClick事件就得到了執行。

 

三、onClick和onLongClick事件能同時發生:

我們通過代碼來演示一下。

1、onTouch事件、onLongClick事件、onClick事件默認是同時執行:(執行的先后順序:onTouch > onLongClick > onClick)

完整版代碼如下:

 1 import android.app.Activity;
 2 import android.os.Bundle;
 3 import android.util.Log;
 4 import android.view.MotionEvent;
 5 import android.view.View;
 6 import android.widget.Button;
 7 
 8 public class MainActivity extends Activity {
 9 
10     private static final String TAG = "MainActivity";
11     private Button btn;
12 
13     @Override
14     protected void onCreate(Bundle savedInstanceState) {
15         super.onCreate(savedInstanceState);
16         setContentView(R.layout.activity_main);
17         btn = (Button) findViewById(R.id.btn);
18 
19         //按鈕的touch事件
20         btn.setOnTouchListener(new View.OnTouchListener() {
21             @Override
22             public boolean onTouch(View v, MotionEvent event) {
23 
24                 switch (event.getAction()) {
25                     case MotionEvent.ACTION_DOWN: //按下的動作
26                         Log.d(TAG, "btn is MotionEvent.ACTION_DOWN");
27                         break;
28 
29                     case MotionEvent.ACTION_MOVE: //滑動的動作
30                         Log.d(TAG, "btn is MotionEvent.ACTION_MOVE");
31                         break;
32 
33                     case MotionEvent.ACTION_UP: //離開的動作
34                         Log.d(TAG, "btn is MotionEvent.ACTION_UP");
35                         break;
36                 }
37 
38                 return false;  //默認的返回值
39             }
40         });
41 
42 
43         //按鈕的onLongClick事件
44         btn.setOnLongClickListener(new View.OnLongClickListener() {
45             @Override
46             public boolean onLongClick(View v) {
47 
48                 Log.d(TAG, "btn is onLongClick");
49 
50                 return false; //默認的返回值
51             }
52         });
53         //按鈕的onClick事件
54         btn.setOnClickListener(new View.OnClickListener() {
55             @Override
56             public void onClick(View v) {
57                 Log.d(TAG, "btn is onClick");
58             }
59         });
60     }
61 
62 }

 

運行程序后,長按按鈕,后台日志如下:

68506973-12bc-41f8-a60c-979ce0afb252

源碼比較長,就不貼出來了,通過查看源碼我們得知,當onTouch事件中的ACTION_DOWN動作執行180ms之后,就會執行onLongClick事件。

那我們現在知道了,如果在一個按鈕上按下的時間過長,onLongClick事件會比onClick事件先執行

 

2、只執行onTouch事件和onLongClick事件,不執行onClick事件:

為了實現這種邏輯,也很簡單,只需要將上方的第50行代碼改為return true就行了,即:將onLongClick方法的返回值改為true,就不會執行onClick事件了。改完代碼之后,后台的運行效果如下:

f2303051-81ad-434d-935b-f0946f53a463

為什么這樣改代碼就可以了呢?這就需要從源碼的角度來理解了。在View.java中的dispatchTouchEvent方法里,ACTION_UP動作里面對onLongTouch事件進行了處理,具體源碼就不展示出來了,這個有點復雜。

 

四、事件傳遞機制調用順序:

ViewGroup的事件傳遞方法:

  • dispatchTouchEvent
  • onInterceptTouchEvent
  • onTouchEvent

View的事件傳遞方法:

  • View的dispatchTouchEvent
  • View的onTouchEvent

注意,只有父的ViewGroup容器才有onInterceptTouchEvent方法。這也很好理解,最小的那個子的view沒必要再攔截了,因為無法繼續向下傳遞事件,是否攔截已經沒有意義了。

 

接下來,我們用LinearLayout代表ViewGroup,用Button代表子View,然后去重寫LinearLayout和Button中的事件傳遞方法,看一下各個方法的調用順序。代碼如下:

(1)MyLinearLayout.java:(重寫LinearLayout中的事件傳遞方法)

 1 package com.example.smyhvae.touchdemo;
 2 
 3 import android.content.Context;
 4 import android.util.AttributeSet;
 5 import android.util.Log;
 6 import android.view.MotionEvent;
 7 import android.widget.LinearLayout;
 8 
 9 /**
10  * Created by smyhvae on 2015/9/11.
11  */
12 public class MyLinearLayout extends LinearLayout {
13 
14     private static final String TAG = "MainActivity";
15 
16     public MyLinearLayout(Context context) {
17         super(context);
18     }
19 
20     public MyLinearLayout(Context context, AttributeSet attrs) {
21         super(context, attrs);
22     }
23 
24     public MyLinearLayout(Context context, AttributeSet attrs, int defStyle) {
25         super(context, attrs, defStyle);
26     }
27 
28     @Override
29     public boolean dispatchTouchEvent(MotionEvent ev) {
30 
31         switch (ev.getAction()) {
32             case MotionEvent.ACTION_DOWN: //按下的動作
33                 Log.d(TAG, "ViewGroup dispatchTouchEvent ACTION_DOWN");
34                 break;
35             case MotionEvent.ACTION_MOVE: //滑動的動作
36                 Log.d(TAG, "ViewGroup dispatchTouchEvent ACTION_MOVE");
37                 break;
38             case MotionEvent.ACTION_UP: //離開的動作
39                 Log.d(TAG, "ViewGroup dispatchTouchEvent ACTION_UP");
40                 break;
41         }
42 
43         return super.dispatchTouchEvent(ev);
44     }
45 
46     @Override
47     public boolean onInterceptTouchEvent(MotionEvent ev) {
48 
49         switch (ev.getAction()) {
50             case MotionEvent.ACTION_DOWN: //按下的動作
51                 Log.d(TAG, "ViewGroup onInterceptTouchEvent ACTION_DOWN");
52                 break;
53             case MotionEvent.ACTION_MOVE: //滑動的動作
54                 Log.d(TAG, "ViewGroup onInterceptTouchEvent ACTION_MOVE");
55                 break;
56             case MotionEvent.ACTION_UP: //離開的動作
57                 Log.d(TAG, "ViewGroup onInterceptTouchEvent ACTION_UP");
58                 break;
59         }
60 
61         return super.onInterceptTouchEvent(ev);
62     }
63 
64     @Override
65     public boolean onTouchEvent(MotionEvent event) {
66 
67         switch (event.getAction()) {
68             case MotionEvent.ACTION_DOWN: //按下的動作
69                 Log.d(TAG, "ViewGroup onTouchEvent ACTION_DOWN");
70                 break;
71             case MotionEvent.ACTION_MOVE: //滑動的動作
72                 Log.d(TAG, "ViewGroup onTouchEvent ACTION_MOVE");
73                 break;
74             case MotionEvent.ACTION_UP: //離開的動作
75                 Log.d(TAG, "ViewGroup onTouchEvent ACTION_UP");
76                 break;
77         }
78 
79         return super.onTouchEvent(event);
80     }
81 }

 

(2)MyButton.java:(重寫Button中的事件傳遞方法,注意:這里面沒有onInterceptTouchEvent方法)

 1 package com.example.smyhvae.touchdemo;
 2 
 3 import android.content.Context;
 4 import android.util.AttributeSet;
 5 import android.util.Log;
 6 import android.view.MotionEvent;
 7 import android.widget.Button;
 8 
 9 /**
10  * Created by smyhvae on 2015/9/11.
11  */
12 public class MyButton extends Button {
13     private static final String TAG = "MainActivity";
14 
15     public MyButton(Context context) {
16         super(context);
17     }
18 
19     public MyButton(Context context, AttributeSet attrs) {
20         super(context, attrs);
21     }
22 
23     public MyButton(Context context, AttributeSet attrs, int defStyle) {
24         super(context, attrs, defStyle);
25     }
26 
27     @Override
28     public boolean dispatchTouchEvent(MotionEvent event) {
29         switch (event.getAction()) {
30             case MotionEvent.ACTION_DOWN: //按下的動作
31                 Log.d(TAG, "View      dispatchTouchEvent ACTION_DOWN");
32                 break;
33             case MotionEvent.ACTION_MOVE: //滑動的動作
34                 Log.d(TAG, "View      dispatchTouchEvent ACTION_MOVE");
35                 break;
36             case MotionEvent.ACTION_UP: //離開的動作
37                 Log.d(TAG, "View      dispatchTouchEvent ACTION_UP");
38                 break;
39         }
40 
41         return super.dispatchTouchEvent(event);
42     }
43 
44     @Override
45     public boolean onTouchEvent(MotionEvent event) {
46         switch (event.getAction()) {
47             case MotionEvent.ACTION_DOWN: //按下的動作
48                 Log.d(TAG, "View      onTouchEvent ACTION_DOWN");
49                 break;
50             case MotionEvent.ACTION_MOVE: //滑動的動作
51                 Log.d(TAG, "View      onTouchEvent ACTION_MOVE");
52                 break;
53             case MotionEvent.ACTION_UP: //離開的動作
54                 Log.d(TAG, "View      onTouchEvent ACTION_UP");
55                 break;
56         }
57 
58         return true;
59     }
60 }

 

上方代碼中,將onTouchEvent方法的返回值修改為true(59行),表示這個子的view希望消費這個事件。

(3)activity_main.xml:

<com.example.smyhvae.touchdemo.MyLinearLayout
    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"
    tools:context=".MainActivity">

    <com.example.smyhvae.touchdemo.MyButton
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按鈕"/>

</com.example.smyhvae.touchdemo.MyLinearLayout>

 

上面的xml中,將我們自定義的MyLinearLayout和MyButton用上了。

(4)MainActivity.java:

 1 package com.example.smyhvae.touchdemo;
 2 
 3 import android.app.Activity;
 4 import android.os.Bundle;
 5 import android.widget.Button;
 6 
 7 public class MainActivity extends Activity {
 8 
 9     private static final String TAG = "MainActivity";
10     private Button btn;
11 
12     @Override
13     protected void onCreate(Bundle savedInstanceState) {
14         super.onCreate(savedInstanceState);
15         setContentView(R.layout.activity_main);
16         btn = (Button) findViewById(R.id.btn);   
17     }
18 }

 

分析之前,我們先記住下面這句話:(記住這句話,分析下面的日志就好理解了)

  在Android中,一切事件處理的開始都是從Down事件開始的,如何你處理了Down事件,其他的事件就都收不到了。

1、按照上面的代碼,后台日志如下:

1e8f1be4-ba35-4338-a3b4-e177936df75d

通過上圖的箭頭處可以看到,事件是傳遞給了子view去消費

 

2、上面的代碼中,將MyLinearLayout.java中onTouchEvent方法返回值修改為true,將MyButton.java中的onTouchEvent方法返回值修改為false,后台日志如下:

ab3a7724-bdfa-487c-b616-ad6bf9dcafc2

通過上圖的箭頭處可以看到,事件是傳遞給了子view,子view說它不消費了,於是又回傳給父的ViewGroup,ViewGroup消費了這個事件

3、上面的代碼中,將MyLinearLayout.java中onTouchEvent方法返回值修改為true,將MyLinearLayout.java中onInterceptTouchEvent方法返回值修改為true,后台日志如下:

e0114a8f-013c-472d-a6b5-dbc9b0e94a75

通過上圖的箭頭處可以看到,此時ViewGroup已經將事件攔截了,所以根本就不會傳遞給子的Veiw,父的ViewGroup自己把事件給消費掉了

 

我的公眾號

下圖是我的微信公眾號(生命團隊id:vitateam),歡迎有心人關注。博客園分享技術,公眾號分享心智

我會很感激第一批關注我的人。此時,年輕的我和你,一無所有;而后,富裕的你和我,滿載而歸。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM