View的事件分发机制

656 阅读6分钟

一、三个关键方法的调用顺序

dispatchTouchEvent / onInterceptTouchEvent / onTouchEvent :

public boolean dispatchTouchEvent: 
用来进行事件的分发。如果事件能够将传递给当前的View,那么此方法一定会被调用,返回结果受当前View的
onTouchEvent和下级View即子View的dispatchTouchEvent方法的影响,鄙视是否消耗当前事件;

public boolean onInterceptTouchEvent:
在dispatchTouchEvent方法内部调用,判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个
事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

public boolean onTouchEvnet:
在dispatchTouchEvent方法内部调用,用来处理点击事件,返回结果表示是否小号当前事件,如果不消耗,则在
同一个事件序列中,当前View无法再次接受到该事件;

伪代码表示关系:

public boolean dispatchTouchEvent(MotionEvent event) {
    booblean consume = false;
    if(onInterceptTouchEvent(evnet)){
        consume = onTouchEvent(event);
    } else {
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
} 

通过上述伪代码中已经将三个方法的关系表示的十分清晰。可以看出点击事件的传递规则,即对于一个根ViewGroup来说,点击事件产生后,首先会传递给自己,此时它的dispatchTouchEvent方法就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true,就表示它要拦截当前事件,接着事件就会交给这个ViewGroup来处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent返回的是false,就表示此ViewGroup不拦截此事件,这是当前事件就会继续传递给他的子View,接着子View的dispatchTouchEvent方法就会被调用,如此反复知道事件最终被处理或舍弃;

二、OnTouchListener、OnTouchEvent 和 onClickListener的关系:

对于一个View的事件的处理,有三个方法可以处理,包括onTouchListener、onTouchEvent和onClickListener方法;

通过一个简单的Demo来分析下这三个事件处理方法的先后关系:

1. 新建一个布局文件activity_main.xml:

<!-- layout: activity_main.xml -->
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.porify.test79.MainActivity">

    <com.example.porify.test79.HelloTextView
        android:id="@+id/tv_hello"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

2. 新建一个自定义的TextView:

为了确定onTouchEvent方法的调用点,我们新建一个简单的自定义View,HelloTextView,继承TextView即可,在此View的onTouchEvent方法中添加相关打印;

// HelloTextView.java
public class HelloTextView extends TextView {
    private static final String TAG = HelloTextView.class.getSimpleName();
    public HelloTextView(Context context) {
        super(context);
    }

    public HelloTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public HelloTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public HelloTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d(TAG, "dispatchTouchEvent: action: " + event.getAction());
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: action: " + event.getAction());
        return super.onTouchEvent(event);
    }
}

3. 对HelloTextView添加事件监听:

对HelloTextView的事件进行监听,并添加相关打印:

// MainActivity.java
public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        HelloTextView helloText = (HelloTextView) findViewById(R.id.tv_hello);
        helloText.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d(TAG, "setOnTouchListener: onTouch: action: " + event.getAction());
                return false;
            }
        });

        helloText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "setOnClickListener: onClick: ");
                //new Exception("onClickListener:onClick").printStackTrace();
            }
        });
    }
}

1) 在OnTouchListener的onTouch中返回false时,打印如下:

 D/HelloTextView: dispatchTouchEvent: action: 0
 D/MainActivity: setOnTouchListener: onTouch: action: 0
 D/HelloTextView: onTouchEvent: action: 0
 D/HelloTextView: dispatchTouchEvent: action: 1
 D/MainActivity: setOnTouchListener: onTouch: action: 1
 D/HelloTextView: onTouchEvent: action: 1
 D/MainActivity: setOnClickListener: onClick: 

可以看出首先调用dispatchTouchEvent -->OnTouchListener:onTouch --> onTouchEvent,最后调用OnClickListener:onClick方法,并且此方法只有在ACTION_UP事件到来时才会被调用;

2)在OnTouchListener的onTouch中返回true时,打印如下:

D/HelloTextView: dispatchTouchEvent: action: 0
D/MainActivity: setOnTouchListener: onTouch: action: 0
D/HelloTextView: dispatchTouchEvent: action: 1
D/MainActivity: setOnTouchListener: onTouch: action: 1

可以看出当onTouchEvent方法中发挥true时,首先调用dispatchTouchEvent方法,然后调用OnTouchListener的onTouch方法后,不会再调用onTouchEvent 和 OnClickListener的onClick方法;

所以,以上三个事件处理方法的调用关系如下:

当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener的onTouch方法会被调用,此后事件如何处理需要看onTouch方法的放回值:

如果返回false,当前View的onTouchEvent方法会被调用;

如果放回true,当前View的onTouchEvent方法不会被调用;

而OnClickListener的onClick方法在事件的末端的ACTION_UP事件到时时被调用;

所以优先级为:OnTouchListener:onTouch > onTouchEvent > OnClickListener:onClick;

三、事件传递的顺序

当一个事件产生以后,其传递遵循如下顺序:

Activity --> Window --> View,

即事件总是先传递给Activity,Activity然后再传递给Window,最后Window再传递给顶级View,顶级View接到事件后,会按照事件分发机制分发事件;

关于事件分发机制,可参考以下图文并茂,浅显易懂的一篇文章

Android事件分发机制 详解攻略,您值得拥有

关于事件传递机制的一些结论:

  1. 同一个事件序列是指从手指接触平面开始,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down开始,中间含有数量不定的move事件,最终以up事件结束;
  2. 正常情况下,一个事件序列只能被一个View拦截且消耗;
  3. 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件能够传递给它的话),并且它的onInterceptTouchEvent不会被嗲用;
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(即onTouchEvent返回false),那么同一个事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用;
  5. 如果View不消耗除ACTIONI_DOWN以外的其他事件,那么这个点击事件会小时,此时父元素的onTouchEvent不会被调用,并且当前View可持续收到后续的事件,最终这些小时的点击事件会传递给Activity处理;
  6. ViewGroup默认不拦截任何事件;Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false;
  7. View没有onInterceptTouchEvent方法,一旦有点击事件出递给他,那么它的onTouchEvent方法就会被调用;
  8. View的onTouchEvent犯法默认都会消耗事件(返回true),除非他是不可点击的(clickable和longClickable同时为false),View的longClickable默认均为false,clickable分情况;
  9. View的enable属性不影响onTouchEvent的默认返回值;
  10. onClick会被调用的前提是当前View是可点击的,并且它收到了DOWN和UP事件;
  11. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素传递给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。


说明:以上内容主要参考《Android开发艺术探索》