android中會經常遇到多個View ViewGroup嵌套的問題,如果想要快速的解決這種問題,就需要對View的事件傳遞有較深入的理解。一次完整的事件傳遞機制,主要是三個階段,分別是事件的分發,攔截和消費。
1 觸摸事件的主要類型
觸摸事件對應的是MotionEvent類,事件的類型主要有如下三種。
- ACTION_DOWN:用戶手指按下的操作,一個按下操作標誌着一次觸摸事件的開始。
- ACTION_MOVE:用戶手指按壓屏幕後,在鬆開之前,如果移動的距離超過一定的閾值,那麼會被判定爲ACTION_MOVE的操作,一般情況下,手指的輕微移動都會觸發一系列的移動事件。
- ACTION_UP:用戶手指離開 屏幕的操作,一次擡起操作標誌者這一次觸摸事件的結束。
在一次觸摸事件操作中,ACTION_DOWN和ACTION_UP這兩個事件是必須的,而ACTION_MOVE視情況而定,如果用戶只點擊了一下屏幕,那麼可能只會監聽到按下和擡起的動作。
2 事件傳遞的三個階段
- 分發(Dispatch):事件的分發對應着dispatchTouchEvent方法,在Android系統中,所有的觸摸事件都是通過這個方法來分發的,方法原型如下:
// 方法值返回true,表示事件被當前視圖消費掉,不再繼續分發事件
// 方法值返回super.dispatchTouchEvent 表示繼續分發該事件
public boolean dispatchTouchEvent(MotionEvent ev)
- 攔截(Intercept):事件的攔截對應着onInterceptTouchEvent方法,這個方法只在ViewGroup及其子類中才存在,在View和Activity中是不存在的,方法原型如下:
// 方法值返回true,表示攔截這個事件,不繼續分發給子視圖,同時交由自身的onTouchEvent方法進行消費
// 方法值返回false或者super.onInterceptTouchEvent表示不對事件進行攔截,需要繼續傳遞給子視圖
public boolean onInterceptTouchEvent(MotionEvent ev)
- 消費(Consume):事件的消費對應着onTouchEvent方法,方法原型如下:
// 方法值返回true,表示當前視圖可以處理對應的事件,事件將不會向上傳遞給父視圖
// 方法值返回false,表示當前視圖不處理這個事件,事件會被傳遞給父視圖的onTouchEvent方法處理
public boolean onTouchEvent(MotionEvent ev)
在Android 系統中,擁有事件傳遞能力的類有以下三種:
- Activity:擁有dispatchTouchEvent和onTouchEvent兩個方法
- ViewGroup:擁有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三個方法
- View:擁有dispatchTouchEvent和onTouchEvent兩個方法
注意:這裏的View是不包括ViewGroup的View控件,比如Button,TextView等本身已經是最小的單位。
3 View的事件傳遞機制
// 自定義MyTextView
public class MyTextView extends TextView {
private static final String TAG = "MyTextView";
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "onTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
自定義一個MyTextView 重寫dispatchTouchEvent()和onTouchEvent()方法,將每個事件觸發都打印Log日誌。在定義一個MainActivity,在MainActivity中監聽MyTextView對象的觸摸和點擊事件。
public class MainActivity extends AppCompatActivity implements View.OnTouchListener, View.OnClickListener {
private static final String TAG = "MainActivity";
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.my_text_view);
mTextView.setOnClickListener(this);
mTextView.setOnTouchListener(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "onTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (v.getId()) {
case R.id.my_text_view :
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(TAG, "MyTextView onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE :
Log.e(TAG, "MyTextView onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP :
Log.e(TAG, "MyTextView onTouch ACTION_UP");
break;
default:
break;
}
break;
default:
break;
}
return false;
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.my_text_view:
Log.e(TAG, "MyTextView onClick");
break;
default:
break;
}
}
}
當我們點擊MyTextView的對象時,打印log如下:
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onClick
可以看出一下結論:
1. 觸摸事件的傳遞流程從dispatchTouchEvent開始,如果不進行人爲干預,則事件會依靠嵌套從外層向裏層傳遞,到達最裏層的view時,就由它的onTouchEvent處理,如果它消費不了就依次從裏向外傳遞,由外層view的onTouchEvent方法進行處理。
2. 如果事件在向內層傳遞過程中人爲干預,事件處理函數返回true,則會導致事件提前被消費掉,內層view將不會接收到這個事件。
3. view控件的事件觸發順序是先執行onTouch方法,在最後才執行onClick方法,如果onTouch返回true,則事件不會繼續傳遞,最後也不會調用onClick方法,如果onTouch返回false,則事件繼續傳遞。
4. 另外,dispatchTouchEvent()方法中還有“記憶”的功能,如果第一次事件向下傳遞到某View,它把事件繼續傳遞交給它的子View,它會記錄該事件是否被它下面的View給處理成功了,(怎麼能知道呢?如果該事件會再次被向上傳遞到我這裏來由我的onTouchEvent()來處理,那就說明下面的View都沒能成功處理該事件);當第二次事件向下傳遞到該View,該View的dispatchTouchEvent()方法機會判斷,若上次的事件由下面的view成功處理了,那麼這次的事件就繼續交給下面的來處理,若上次的事件沒有被下面的處理成功,那麼這次的事件就不會向下傳遞了,該View直接調用自己的onTouchEvent()方法來處理該事件。
4 ViewGroup的事件傳遞機制
ViewGroup是作爲view控件的容器存在的,ViewGroup擁有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三個方法,可以看出和View的唯一區別是多了一個onInterceptTouchEvent方法。我們自定義一個MyRelativeLayout 繼承RelativeLayout。
// 自定義RelativeLayout
public class MyRelativeLayout extends RelativeLayout{
private static final String TAG = "MyRelativeLayout";
public MyRelativeLayout(Context context) {
super(context);
}
public MyRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE :
Log.e(TAG,"onInterceptTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP :
Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onInterceptHoverEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "onTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.onTouchEvent(event);
}
}
修改xml文件,將這個Layout作爲MyTextView的容器,如下:
<?xml version="1.0" encoding="utf-8"?>
<com.example.jmf.advancedlevel.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.jmf.advancedlevel.MyTextView
android:id="@+id/my_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="請點擊我"
android:textSize="20sp" />
</com.example.jmf.advancedlevel.MyRelativeLayout>
運行,點擊MyTextView,打印Log日誌如下:
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_DOWN
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onTouch ACTION_UP
com.example.jmf.advancedlevel E/MyTextView: onTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: MyTextView onClick
從上面的日誌情況得到如下結論:
1. 觸摸事件的傳遞順序是由Activity到ViewGroup,再由ViewGroup遞歸傳遞給它的子View
2. ViewGroup通過onInterceptTouchEvent方法對事件進行攔截,如果該方法返回true,則事件不會繼續傳遞給子View,如果返回false或者super.onInterceptTouchEvent,則事件會繼續傳遞給子View。
3. 在子View中對事件進行消費後,ViewGroup將接收不到任何事件
5關於事件的攔截和反攔截
1.5.1 怎麼攔截事件?很簡單,複寫ViewGroup的onInterceptTouchEvent方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
Log.e(TAG,"onInterceptTouchEvent ACTION_DOWN");
return true;
case MotionEvent.ACTION_MOVE :
Log.e(TAG,"onInterceptTouchEvent ACTION_MOVE");
return true;
case MotionEvent.ACTION_UP :
Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
return true;
}
return true;
}
默認返回false 或者super.onInterceptTouchEvent,表示是不攔截事件,現在運行,如下:
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MyRelativeLayout: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_DOWN
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_MOVE
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_MOVE
com.example.jmf.advancedlevel E/MainActivity: dispatchTouchEvent ACTION_UP
com.example.jmf.advancedlevel E/MainActivity: onTouchEvent ACTION_UP
Log中可以看到MyTextView中什麼事件也接受不到,如果你在MOVE return true , 則子View在MOVE和UP都不會捕獲事件。原因很簡單,當onInterceptTouchEvent(ev) return true的時候,會把mMotionTarget 置爲null ;
1.5.2 怎麼實現反攔截,就是不讓父類攔截子View?
Android給我們提供了一個方法:requestDisallowInterceptTouchEvent(boolean) 用於設置是否允許攔截,我們在子View的dispatchTouchEvent中直接這麼寫:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// getParent().requestDisallowInterceptTouchEvent(true); 這樣即使ViewGroup在MOVE的時候return true,子View依然可以捕獲到MOVE以及UP事件。
getParent().requestDisallowInterceptTouchEvent(true);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "dispatchTouchEvent ACTION_CANCEL");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
從源碼也可以解釋:
ViewGroup MOVE和UP攔截的源碼是這樣的:
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
當我們把disallowIntercept設置爲true時,!disallowIntercept直接爲false,於是攔截的方法體就被跳過了~
注:如果ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN裏面直接return true了,那麼子View是木有辦法的捕獲事件的~~~
6 總結
1、如果ViewGroup找到了能夠處理該事件的View,則直接交給子View處理,自己的onTouchEvent不會被觸發;
2、可以通過複寫onInterceptTouchEvent(ev)方法,攔截子View的事件(即return true),把事件交給自己處理,則會執行自己對應的onTouchEvent方法
3、子View可以通過調用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup對其MOVE或者UP事件進行攔截;