事件傳遞雖然算不上某個單獨的知識點,但是在實際項目開發中肯定會碰到,如果不明白其中的原理,那在設計各種滑動效果時就會感到很困惑。
關於事件的傳遞,我們可能會有以下疑問:
事件是如何傳遞的
事件是如何處理的
自定義view的時候,事件也沖突了怎么解決
帶着這三個疑問,我們來總結一下事件傳遞機制是怎么回事。
一、事件分發的原理:
1、事件是如何傳遞的:
(1)首先由Activity分發,分發給根View,也就是DecorView(DecorView為整個Window界面的最頂層View)
(2)然后由根View分發到子的View
如下圖所示:
再來看下面這張圖:(這張圖是整個事件傳遞機制的核心)
上圖顯示:
在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截。onInterceptTouchEvent方法:
返回true代表不允許事件繼續向子View傳遞,將會觸發當前View的onTouchEvent(),進行事件的消費;
返回false代表不對事件進行攔截,事件可以傳遞給孩子
默認返回false
2、事件是如何處理的:
再來看下面這張圖:
上圖顯示:子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事件,現在運行程序,點擊按鈕,后台打印的日志如下:
通過上方日志我們可以看到,onTouch事件是比onClick事件先執行的。
備注:這里提示一下,如果我們僅僅只是用手指點擊按鈕,然后馬上松開,onTouch事件中只會執行ACTION_DOWN和ACTION_UP動作;如果用手機點擊按鈕,並且手指還在按鈕上滑動了一會兒,那么滑動的過程中,ACTION_MOVE動作就會不停的執行。現在我們應該能明白這三個動作的含義了吧?
3、只執行onTouch事件,不執行onClick事件:
如果按鈕的onTouch和onClick方法同時執行,在有些情況下不太滿足產品的需求。那如果只想執行onTouch事件,不執行onClick事件,該怎么做呢?很簡單,只需要在上方代碼中,將第39行的代碼改為return true,就行了,即:將onTouch方法的返回值改為true,就會只執行onTouch事件,不執行onClick事件。改完代碼之后,后台的運行效果如下:
為什么這樣改代碼就可以了呢?這就需要從源碼的角度來理解了。
button按鈕中沒有dispatchTouchEvent方法,需要去它的父類View.java中去找dispatchTouchEvent方法。源碼如下所示:
上圖分析:紅框部分的onTouch()方法默認是返回false,所以就會執行藍框部分的代碼,即:調用onTouchEvent()方法。而onTouchEvent方法中,會在ACTION_UP動作里面會去初始化onClick事件。如下圖所示:
於是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 }
運行程序后,長按按鈕,后台日志如下:
源碼比較長,就不貼出來了,通過查看源碼我們得知,當onTouch事件中的ACTION_DOWN動作執行180ms之后,就會執行onLongClick事件。
那我們現在知道了,如果在一個按鈕上按下的時間過長,onLongClick事件會比onClick事件先執行。
2、只執行onTouch事件和onLongClick事件,不執行onClick事件:
為了實現這種邏輯,也很簡單,只需要將上方的第50行代碼改為return true就行了,即:將onLongClick方法的返回值改為true,就不會執行onClick事件了。改完代碼之后,后台的運行效果如下:
為什么這樣改代碼就可以了呢?這就需要從源碼的角度來理解了。在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、按照上面的代碼,后台日志如下:
通過上圖的箭頭處可以看到,事件是傳遞給了子view去消費。
2、上面的代碼中,將MyLinearLayout.java中onTouchEvent方法返回值修改為true,將MyButton.java中的onTouchEvent方法返回值修改為false,后台日志如下:
通過上圖的箭頭處可以看到,事件是傳遞給了子view,子view說它不消費了,於是又回傳給父的ViewGroup,ViewGroup消費了這個事件。
3、上面的代碼中,將MyLinearLayout.java中onTouchEvent方法返回值修改為true,將MyLinearLayout.java中onInterceptTouchEvent方法返回值修改為true,后台日志如下:
通過上圖的箭頭處可以看到,此時ViewGroup已經將事件攔截了,所以根本就不會傳遞給子的Veiw,父的ViewGroup自己把事件給消費掉了。
我的公眾號
下圖是我的微信公眾號(生命團隊id:vitateam
),歡迎有心人關注。博客園分享技術,公眾號分享心智。
我會很感激第一批關注我的人。此時,年輕的我和你,一無所有;而后,富裕的你和我,滿載而歸。