Android Touch事件的傳遞機制

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事件進行攔截;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章