[宣告] 本文來自《顧浩鑫-Android高階進階》第一章 的學習筆記,發文目的在於傳遞分享知識。如涉及文章內容、版權和其它問題,請與本人聯絡,我將在第一時間刪除內容!
前言:在Android開發中,經常會遇到觸控事件衝突,比如ViewPager的輪播圖跟Fragment的划動事件衝突,或者輪播圖跟下拉事件衝突,自定義view的事件處理等,本文章將會詳細介紹Activity、View、ViewGroup三者的觸控事件傳遞機制,傳遞包括三個階段:分發、攔截、消費。
複製程式碼
本文章將會詳細介紹Activity、View、ViewGroup三者的觸控事件傳遞機制,傳遞包括三個階段:分發、攔截、消費。
一.觸控事件的型別
觸控事件對應的是 MotionEvent 類,事件型別主要有三種:
- ACTION_DOWN:使用者按下操作,表示一次觸控事件的開始。
- ACTION_MOVE:在按下的情況下,進行移動。輕微的移動都會傳遞到該事件。
- ACTION_UP:使用者手指離開螢幕,表示一次觸控事件的
注 :如果使用者僅僅的是點選而已,則只會執行到 ACTION_DOWN 和 ACTION_UP 兩個事件,不會執行到 ACTION_MOVE 事件。所以 ACTION_DOWN 和 ACTION_UP 是事件是必須的。
二.觸控事件的傳遞階段
1.分發(Dispatch)
在Android系統中所有的觸控事件都是由 dispatchTouchEvent 方法進行分發的。該方法中判斷事件是被消費(return true),還是繼續分發給子檢視處理(return super.dispatchTouchEvent),如果當前檢視是ViewGroup或者其子類,則會呼叫onInterceptTouchEvent 判斷是否截攔。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
複製程式碼
2.截攔(Intercept)
事件的截攔 InterceptTouchEvent 只存在於ViewGroup及其子類,activity和View是不存在該方法。該方法判斷事件是被截攔 (return true)並交給自身的 OnToucEvent 方法進行消費,還是繼續傳遞給子檢視(return super.InterceptTouchEvent 或者 return false)。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
複製程式碼
3.消費(Consume)
事件的消費通過 OnTouchEvent 方法判斷,是被消費(return true),還是不處理(return false)並將事件傳遞給父檢視的 OnTouchEvent 方法進行處理。
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
複製程式碼
所有擁有事件傳遞能力的類:
Activity: 擁有dispatchTouchEvent 、OnTouchEvent
ViewGroup: 擁有dispatchTouchEvent 、OnInterceptTouchEvent 、OnTouchEvent
View:擁有dispatchTouchEvent 、OnTouchEvent
三、View的事件傳遞機制
3.1 dome
雖然說ViewGroup是View的子類,但是這是說的View指的是除ViewGroup之外的View控制元件子類,首先定義一個MyTextView繼承TextView,列印每次事件的觸發以變了解事件傳遞的流程。
MyTextView 類
public class MyTextView extends TextView {
private String tag = "MyTextView";
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "dispatchTouchEvent ACTION_DOWN");
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "onTouchEvent ACTION_DOWN");
break;
}
return super.onTouchEvent(event);
}
}
複製程式碼
定義一個MainActivity來展現這個MyTextView,同時設定點選(onClick)和觸控(onTouch)監聽。 MainActivity 類
public class MainActivity extends AppCompatActivity implements View.OnClickListener,View.OnTouchListener{
private MyTextView mMyTextView;
private String tag = "MainActiviy";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mMyTextView = findViewById(R.id.text_view);
// 點選監聽
mMyTextView.setOnClickListener(this);
// 觸碰監聽
mMyTextView.setOnTouchListener(this);
}
// MyTextView 點選事件
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.text_view:
Log.i(tag, "MyTextView onClick");
break;
}
}
// MyTextView 觸碰事件
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "MyTextView onTouch ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "MyTextView onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "MyTextView onTouch ACTION_DOWN");
break;
}
return false;
}
// Activity 的事件分發
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "dispatchTouchEvent ACTION_DOWN");
break;
}
return super.dispatchTouchEvent(ev);
}
// Activity 的事件消費
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "onTouchEvent ACTION_DOWN");
break;
}
return super.onTouchEvent(event);
}
}
複製程式碼
3.2 列印日誌
執行後,點選Text View反饋的列印日誌
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_DOWN
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_UP
03-28 08:05:15.044 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onClick
複製程式碼
dispatchTouchEvent 、 OnTouchEvent 這兩個方法的返回值存在三種情況:
- 直接返回true。
- 直接返回false。
- 返回父類同名方法,super.dispatchTouchEvent 或者 super.OnTouchEvent。
由於擁有不同的返回值,所以事件傳遞流程也有不同,經過不斷修改返回值測試,最終得到了點選事件的流程圖,ACTION_DOWN 和 ACTION_UP 事件的傳遞流程是相同的。
3.3 事件傳遞流程圖
從上面的流程圖可以得出結論:- 觸控事件是從 dispatchTouchEvent 開始的,預設返回父類同名方法 super ,事件將會依照巢狀層次從外向內傳遞( MainActivity 到 MyTextView ),到達最內層的 View 時,將由 View 的 OnTouchEvent 方法處理,該方法返回 true 時進行消費不再傳遞,返回 false 時再由內向外傳遞,由外層的 OnTouchEvent 處理。
- 如果外層向內層傳遞過程中,人為干擾返回 true 消費,則不會繼續繼續像內部傳遞。
- View 的事件控制順序先執行 onTouch 再執行 onClick ,如果 onTouch 返回 true 消費,則不會繼續傳遞,也不會執行 onClick 方法。
四、ViewGroup的事件傳遞機制
4.1 dome
ViewGroup 是 View 的控制元件容器存在,擁有 dispatchTouchEvent 、 onInterceptTouchEvent 和 onTouchEvent 三個方法,比 View 多了一個 onInterceptTouchEvent 方法。為了更好的觀察,我們需要自定義 MyRelativeLayout 繼承 RelativeLayout 。
MyRelativeLayout類
public class MyRelativeLayout extends RelativeLayout {
private final static String tag = "MyRelativeLayout";
public MyRelativeLayout(Context context) {
super(context);
}
public MyRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "dispatchTouchEvent ACTION_DOWN");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "onInterceptTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "onInterceptTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "onInterceptTouchEvent ACTION_DOWN");
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "onTouchEvent ACTION_DOWN");
break;
}
return super.onTouchEvent(event);
}
}
複製程式碼
main_activity.xml 檔案
<?xml version="1.0" encoding="utf-8"?>
<com.mvp.chenzhesheng.androidadvance.MyRelativeLayout
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"
>
<com.mvp.chenzhesheng.androidadvance.MyTextView
android:id="@+id/text_view"
android:clickable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
</com.mvp.chenzhesheng.androidadvance.MyRelativeLayout>
複製程式碼
4.2 列印日誌
04-02 08:47:57.980 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.010 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_DOWN
04-02 08:47:58.010 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_DOWN
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: dispatchTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: onInterceptTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_UP
04-02 08:47:58.210 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_UP
04-02 08:47:58.210 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_UP
04-02 08:47:58.260 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onClick
複製程式碼
可以看到 MainActivity 和 MyTextView 的事件傳遞處理中新增了一層 MyRelativeLayout 。通過不同返回值測試,得到一套流程圖。
4.3 流程圖
從上面的流程圖可以得出結論:- 觸控事件傳遞是從 Activity 傳遞到 ViewGroup ,再傳遞到 View 。如果中間沒有 ViewGroup 則直接從 Activity 傳遞到 View 。
- ViewGroup 通過 onInterceptTouchEvent 方法對事件進行截攔,如果返回 false 或者 super.onInterceptTouchEvent ,則事件會繼續傳遞給子 View 。
- 子 View 中對事件進行消費後,ViewGroup 將不會接收到任何事件。
五.總結
- 事件分發是由外到內,從 Activity 到具體的子 View ;
- 事件處理消費是由內到外,從子 View 到最外層 Activity ;
- 事件攔截只存在於 ViewGroup 中;
- 掌握事件傳遞機制可以更好的進行事件處理,無論是自定義 View 還是閱讀 Framework 層原始碼都需要對事件傳遞進行學習,才能更精緻的開發應用。