View的事件分发机制

1.点击事件的传递规则

点击事件的分发过程由3个方法完成,分别是:

  • public boolean dispatchTouchEvent(MotionEvent event)
  • public boolean onInterceptTouchEvent(MotionEvent event)
  • public boolean onTouchEvent(MotionEvent event)

public boolean dispatchTouchEvent(MotionEvent event)
用来进行事件分发。如果事件能够传递给当前View,那么次方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent的影响,表示是否消耗当前事件。

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

什么是事件序列?一个事件序列是指从手指触摸屏幕开始,到手指离开屏幕结束,这个过程中产生的一系列事件。一个事件序列以ACTION_DOWN事件开始,中间可能经过若干个MOVE,以ACTION_UP事件结束。

public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,true:消耗,false:不消耗。如果不消耗,则在同一事件序列中,当前View无法再次接受到事件。

它们之间的关系可以用下面的伪代码表示:

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

事件传递规则:
对于一个根ViewGroup来说,当发生点击事件时,首先会传递给自己,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent返回true,就表示ViewGroup自己要拦截点击事件,而点击事件就会交由ViewGroup自己处理,这时ViewGroup的onTouchEvent就会调用。如果ViewGroup的onInterceptTouchEvent返回false,就表示ViewGroup不需要拦截点击事件,这时点击事件就会传递给子View,这时子View就会调用dispatchTouchEvent,如此反复直到事件被处理掉。

当一个View处理事件时,如果当前的View设置了OnTouchListener,那么OnTouchListener中的onTouch方法就会被调用。这时事件如何处理还要看onTouch的返回值,如果返回true那么View的onTouchEvent方法就不会被调用,如果返回false那么View的onTouchEvent方法还是会被调用的。
优先级的高低,从高到低分别是:onTouchListener–>onTouchEvent–>onClickListener

以下结论有助于理解View的分发机制
1.正常情况下,一个事件序列只能被一个View拦截且消耗。因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此用一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View。

2.某个View一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用。

3.某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。

4.如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。

5.ViewGroup默认不拦截任何事件。

6.View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

7.View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的。View的longClickable属性都默认为false。

8.View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只有它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。

9.onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。

10.事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

2.事件的分发过程

当一个点击事件产生后,它的传递过程遵循如下的顺序:
Activity–>Window–>View
点击事件用MotionEvent来表示,当一个点击操作发生时,最先传递给Activity,再由Activity的dispatchTouchEvent来进行事件分发。先是由Activity内部的Window(实现类时PhoneWindow)将事件传给DecorView(这个DecorView就是setContentView的父容器),由于DecorView是继承自FrameLayout而且是父View(ViewGroup),最后在将事件传递给下面的子View。

下面以一个例子的方法说明事件的分发过程:
在Activity中创建一个ViewGroup(father),再在这个ViewGroup(father)中创建一个ViewGroup(child),最后在ViewGroup(child)中放一个ImageView,代码结构如下:
这里写图片描述
下面是具体的代码
Log.java

public class Log {
    public static void d(String tag, String msg) {
        android.util.Log.e(tag,msg);
    }

    public static void w(String tag, String msg) {
        android.util.Log.w(tag, msg);
    }

    public static void e(String tag, String msg) {
        android.util.Log.e(tag, msg);
    }

    public static void i(String tag, String msg) {
        android.util.Log.i(tag, msg);
    }

}

MyImageView.java

public class MyImageView extends ImageView {
    public MyImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public boolean dispatchTouchEvent(MotionEvent ev) {
        android.util.Log.e("eventTest", "MyImageView | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.dispatchTouchEvent(ev);

    }


    public boolean onTouchEvent(MotionEvent ev) {
        android.util.Log.d("eventTest", "MyImageView | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
         return super.onTouchEvent(ev);

    }
}

TouchEventActivity.java

public class TouchEventActivity extends Activity {

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e("eventTest", "Activity | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.dispatchTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("eventTest", "Activity | onTouchEvent --> " + TouchEventUtil.getTouchAction(event.getAction()));
        return super.onTouchEvent(event);
    }

}

TouchEventChilds.java

public class TouchEventChilds extends LinearLayout {

    public TouchEventChilds(Context context) {
        super(context);
    }

    public TouchEventChilds(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e("eventTest", "Childs | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.dispatchTouchEvent(ev);

    }

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e("eventTest", "Childs | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.onInterceptTouchEvent(ev);

    }

    public boolean onTouchEvent(MotionEvent ev) {
        android.util.Log.e("eventTest", "Childs | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));

        return super.onTouchEvent(ev);
    }


}

TouchEventFather.java

public class TouchEventFather extends LinearLayout {

    public TouchEventFather(Context context) {
        super(context);
    }

    public TouchEventFather(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e("eventTest", "Father | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.dispatchTouchEvent(ev);

    }

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("eventTest", "Father | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.onInterceptTouchEvent(ev);

    }

    public boolean onTouchEvent(MotionEvent ev) {
        Log.e("eventTest", "Father | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
        return super.onTouchEvent(ev);

    }

}

TouchEventUtil.java

public class TouchEventUtil {

    public static String getTouchAction(int actionId) {
        String actionName = "Unknow:id=" + actionId;
        switch (actionId) {
        case MotionEvent.ACTION_DOWN:
            actionName = "ACTION_DOWN";
            break;
        case MotionEvent.ACTION_MOVE:
            actionName = "ACTION_MOVE";
            break;
        case MotionEvent.ACTION_UP:
            actionName = "ACTION_UP";
            break;
        case MotionEvent.ACTION_CANCEL:
            actionName = "ACTION_CANCEL";
            break;
        case MotionEvent.ACTION_OUTSIDE:
            actionName = "ACTION_OUTSIDE";
            break;
        }
        return actionName;
    }

}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<com.qqyumidi.event.TouchEventFather xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="320dp"
    android:layout_height="480dp"
    android:background="#468AD7"
    android:gravity="center"
    android:orientation="vertical">

    <com.qqyumidi.event.TouchEventChilds
        android:gravity="center"
        android:id="@+id/childs"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center"
        android:background="#66ff0000">

       <com.qqyumidi.event.MyImageView
            android:scaleType="fitXY"
            android:background="#66ff0000"
            android:layout_gravity="center"
            android:layout_width="200dip"
            android:layout_height="200dip"
            android:src="@drawable/mm" />


    </com.qqyumidi.event.TouchEventChilds>

</com.qqyumidi.event.TouchEventFather>

运行代码,效果如图
这里写图片描述

1.什么都不做,即每一个View和ViewGroup、Activity都是默认返回super.XXX方法时, 当点击中间的MyImageView时,打印的log如下:

这里写图片描述

点击MyImageView时,按理来说MyImageView应该消费了点击事件,但是从log中可以看出ACTION_DOWN并没有在MyImageView中消费掉,而是最终又传到了Activity中。接下来的ACTION_UP并没有传到Father和Childs中,而是直接交由Activity处理,这样印证了上面结论3。可以看出View默认在onTouchEvent中返回false,也就是View默认是不消费事件的。
这里要提一点的是,处理和消费的概念,它们并不是同一个意思。处理:比如说点击事件在OnTouchEvent中处理,只是回调了OnTouchEvent方法,事件不一定消费掉了。消费:事件已经消耗了,不会再继续传递下去。

2.当MyImageView的onTouchEvent返回true时,点击MyImageView,打印的log如下:

这里写图片描述

从log中可以看到点击事件的ACTION_DOWN在MyImageView中已经停止传递了,接下来的ACTION_UP也会传递到MyImageView中,并在MyImageView中消费掉。它与上面的子View不消费掉事件,最终交给Activity消费不同,子View消费掉ACTION_DOWN事件后,ACTION_UP 事件还是会经过父View一层一层传递到子View,最终由子View消费掉。

3.在TouchEventFather的onInterceptTouchEvent返回true,打印的log如下:
这里写图片描述

从log中可以看出,一旦父View拦截了事件,那么接下来的一序列事件都会交由父View消费。

总结一下事件分发的过程:

点击事件到达顶级View(一般是ViewGroup)以后,一般会调用ViewGroup的dispatchTouchEvent方法,然后的逻辑是这样的:如果顶级ViewGroup的拦截事件即onInterceptTouchEvent返回true,则事件由ViewGroup处理,如果这时候ViewGroup的OnTouchListener被设置,则OnTouch会被调用,否则OnTouchEvent被调用,如果在OnTouch中返回true则OnTouchEvent就不会被调用。在OnTouchEvent中,如果设置了OnClickListener,则OnClick会被调用。如果顶级ViewGroup不拦截事件,则事件会被传递给点击事件链所在的子View,这时子View的dispatchTouchEvent就会被调用。到此为止,事件已经从顶级View传递到了下一级View,接下来的传递过程和顶级的View是一样的,反复如此,就可完成View事件的分发。

参考《Android开发艺术探索》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值