事件分发机制再探秘

LinearLayout里面放一个Button

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="match_parent"
    tools:context=".ButtonActivity">
    <com.view.MyLL
        android:id="@+id/ll"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.view.MyButton
            android:id="@+id/b1"
             android:text="按钮"
            android:background="@color/colorAccent"
            android:layout_width="100dp"
            android:layout_height="100dp" />
    </com.view.MyLL>

</LinearLayout>

在这里插入图片描述

MyButton.java


public class MyButton extends android.support.v7.widget.AppCompatButton {
    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    private String msg = "";
    public MyButton(Context context) {
        super(context);
    }

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

    public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i(T.TAG, "MyButton dispatchTouchEvent: "+msg+ Utils.getAction(ev));
        return super.dispatchTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(T.TAG, "MyButton onTouchEvent: "+msg+ Utils.getAction(event));
        return super.onTouchEvent(event);
    }


}

MyLL.java

public class MyLL extends LinearLayout {
    public MyLL(Context context) {
        super(context);
    }

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

    public MyLL(Context context,  AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i(T.TAG, "MyLL dispatchTouchEvent: "+ Utils.getAction(ev));
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i(T.TAG, "MyLL onInterceptTouchEvent: "+ Utils.getAction(ev));
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(T.TAG, "MyLL onTouchEvent: "+ Utils.getAction(event));
        return super.onTouchEvent(event);
    }
}

1 手指点击MyLL中的白色区域再松开,回调日志是什么?

 I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_DOWN
 I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_DOWN
 I/liangchaojie: MyLL onTouchEvent: MotionEvent.ACTION_DOWN

你以为是我截取的时候少日志了?是不是还在疑惑不应该还有一个ACTION_UP事件么?

其实并没有ACTION_UP事件!

ACTION_DOWN这个事件是事件分发的源头,ACTION_DOWN这个事件最终被谁消费,那么后续的事件正常情况下也会交给谁处理,直到处理完最后一次ACTION_UP事件

回过头来看看日志,我们可以猜测是这样的原因:

MyLL在接受ACTION_DOWN事件之后发现自己无法消费该事件,告知父布局自己无法消费,父布局得知以后不再把ACTION_MOVE ACTION_UP等事件交给MyLL去做而是自己处理

这样的猜想合理不合理呢?我们看下源码吧super.onTouchEvent(event)调用的是View的onTouchEvent(event)

View源码onTouchEvent:

源码1.0
 public boolean onTouchEvent(MotionEvent event) {
     //....
      if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
          //...
          return true;
      }
      return false;
 }

默认情况下,我们的MyLL回调最后结果是返回false,即不消费此事件那么父布局将不再把后续的触摸事件分发给MyLL

1.1如果我们这个时候将xml文件增加一句话 android:clickable=“true”
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="match_parent"
    tools:context=".ButtonActivity">
    <com.view.MyLL
        android:id="@+id/ll"
        android:clickable="true"  //加这段代码
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.view.MyButton
            android:id="@+id/b1"
            android:text="按钮"
            android:background="@color/colorAccent"
            android:layout_width="100dp"
            android:layout_height="100dp" />
    </com.view.MyLL>

</LinearLayout>

再看下点击的结果

2019-06-27 11:42:20.062 3620-3620/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:42:20.062 3620-3620/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:42:20.062 3620-3620/com.justtest I/liangchaojie: MyLL onTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:42:20.108 3620-3620/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 11:42:20.108 3620-3620/com.justtest I/liangchaojie: MyLL onTouchEvent: MotionEvent.ACTION_UP

这样就没问题了,确实可以看到完整的事件分发了

1.2 如果我们给MyLL设置监听setOnClickListener
myLL.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.i(T.TAG, "onClick: myLL");
    }
});
2019-06-27 11:48:51.114 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:48:51.114 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:48:51.114 3923-3923/com.justtest I/liangchaojie: MyLL onTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 11:48:51.170 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 11:48:51.170 3923-3923/com.justtest I/liangchaojie: MyLL onTouchEvent: MotionEvent.ACTION_UP
2019-06-27 11:48:51.174 3923-3923/com.justtest I/liangchaojie: onClick: myLL

发现也可以正常的进行事件分发了?为什么?为啥设置了监听就可以走事件分发了呢?

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

我不管你之前是不是clickable,只要你设置了监听我就让你可以点击,也就是说这里其实还是用1.1的这种方法

如果你足够细心的话你会发现ACTION_DOWN事件走了三个回调方法dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent可是ACTION_UP只走了dispatchTouchEvent和onTouchEvent,你想问onInterceptTouchEvent去哪里了呢?

我在之前的博客里面提到过这个问题,如果一个DOWN事件被一个ViewGroup消费了,那么后续事件就不再执行onInterceptTouchEvent方法询问是否拦截了,因为他之前已经知道了自己要干而不是交给子view去干,这样想起来也合理

2 手指点击MyButton区域再松开,回调日志是什么?

2019-06-27 12:57:09.200 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 12:57:09.200 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 12:57:09.200 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 12:57:09.200 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 12:57:09.267 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 12:57:09.267 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_UP
2019-06-27 12:57:09.267 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 12:57:09.267 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_UP

这个就是最正常的逻辑,也是基本上大部分事件分发的回调情况

3 手指点击MyButton红色区域不松手慢慢移动到白色MyLL区域再松手,回调日志是什么?

2019-06-27 13:17:41.783 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 13:17:41.783 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 13:17:41.783 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 13:17:41.783 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_DOWN
2019-06-27 13:17:42.077 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.077 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.077 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.077 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.089 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.090 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_MOVE
....这里省略太多重复的ACTION_MOVE.......
2019-06-27 13:17:42.518 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.518 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.527 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.527 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.528 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.528 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 13:17:42.530 3923-3923/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 13:17:42.530 3923-3923/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_UP
2019-06-27 13:17:42.530 3923-3923/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 13:17:42.530 3923-3923/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_UP

这里就产生有一个问题了,我手都离开了MyButton但是为什么MyButton还在回调触摸事件呢?

#####做android三年了,如果不做这个实验,我还会认为手指离开MyButton,MyButton就不会回调分发事件

我们再回顾上面问题1的结论

ACTION_DOWN这个事件是事件分发的源头,ACTION_DOWN这个事件最终被谁消费,那么后续的事件正常情况下也会交给谁处理,直到处理完最后一次ACTION_UP事件

我们触摸MyButton的时候ACTION_DOWN事件最终是交给MyButton处理的,所以只要没有ACTION_UP事件那么后续的事件都是交给MyButton处理,所以才会看到上面日志的情况,第二次看到这个理论是不是加深了对其的理解呢?

#####3.1那我们现在再考虑一个问题:

在这里插入图片描述
手指从Button1点击然后不松手慢慢移动到Button2再松手,这个时候会走谁的onClickListener事件?还是都不走或者都走?

首先Button2是不会走的,因为他压根就没有接收到触摸事件,因为Button2没有ACTION_DOWN事件传入,并没有对其分发触摸事件,在ACTION_MOVE的过程中他的角色只是LinearLayout的一部分而已。

那么Button1走了吗?这个是很多人困惑的问题。

你可能会说:不走,因为Button1没有接收到ACTION_UP事件,只有接收了ACTION_UP事件,走一个完整的事件分发流程才可以执行onClickListener回调。

Button1没有接收到ACTION_UP事件吗?其实不是!他接收到了,我们看到问题3的日志就知道,即使不在button区域但是也可以收到回调事件,因为ACTION_DOWN是button消费的,只要事件没有结束那么就继续交给其处理,所以手指离开Button1之后,不管你是否move回到button1,其实button1都是可以接收到ACTION_DOWN ACTION_UP事件的。

你也可能会说:走,因为在button1上进行了一个完整的ACTION_DOWN和ACTION_UP操作,是可以满足条件的

那么真相是如何呢?

....之前的太多就不展示了
2019-06-27 16:49:05.588 8161-8161/com.justtest I/liangchaojie: onTouch: mButton
2019-06-27 16:49:05.588 8161-8161/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_MOVE
2019-06-27 16:49:05.591 8161-8161/com.justtest I/liangchaojie: MyLL dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 16:49:05.591 8161-8161/com.justtest I/liangchaojie: MyLL onInterceptTouchEvent: MotionEvent.ACTION_UP
2019-06-27 16:49:05.591 8161-8161/com.justtest I/liangchaojie: MyButton dispatchTouchEvent: MotionEvent.ACTION_UP
2019-06-27 16:49:05.591 8161-8161/com.justtest I/liangchaojie: onTouch: mButton
2019-06-27 16:49:05.591 8161-8161/com.justtest I/liangchaojie: MyButton onTouchEvent: MotionEvent.ACTION_UP

#####Android手机的话Button1不走,你要是换成ios手机是走的,毕竟系统不一样

为什么不走呢?确实是把完整的事件流走了一遍呢!我们这个时候再看看View的onTouchEvent源码

###源码1.1

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            //注意mIgnoreNextUpEvent这个字段
            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                           
                            removeLongPressCallback();
                          if (!focusTaken) {
                                
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                //这段代码就是我们进行onClick操作的代码
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
              }
              mIgnoreNextUpEvent = false;
              break;
      //.........................

mIgnoreNextUpEvent这个字段是这个问题的核心,这个字段表示是否应该忽略下一次ACTION_UP事件,为什么需要忽略呢?

首先这个字段默认值是false

private boolean mIgnoreNextUpEvent;

当mIngoreNextUpEvent=true的时候我们将不进行onClick操作

当mIngoreNextUpEvent=false的时候进行onClick操作

那我们看下什么时候mIngoreNextUpEvent=true呢?

 case MotionEvent.ACTION_BUTTON_PRESS:
                if (isContextClickable() && !mInContextButtonPress && !mHasPerformedLongPress
                        && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
                        || actionButton == MotionEvent.BUTTON_SECONDARY)) {
                    if (performContextClick(event.getX(), event.getY())) {
                        mInContextButtonPress = true;
                        setPressed(true, event.getX(), event.getY());
                        removeTapCallback();
                        removeLongPressCallback();
                        return true;
                    }
                }
                break;
case MotionEvent.ACTION_BUTTON_RELEASE:
    if (mInContextButtonPress && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
            || actionButton == MotionEvent.BUTTON_SECONDARY)) {
        mInContextButtonPress = false;
        mIgnoreNextUpEvent = true; //整段代码仅此一处设置为了true
    }
    break;

ACTION_BUTTON_PRESS???我们知道isPressed()代表了按钮是否被按压,难道是系统判断了button没有被按压直接mIgnoreNextUpEvent设置成了true?

没错,打了日志你会发现,当你手指按压和不按压的时候isPressed()返回不同的值

//第一个默认是false
2019-06-27 17:06:52.563 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
//按压之后显示true
2019-06-27 17:06:52.754 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.771 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.788 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.805 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.824 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.839 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
2019-06-27 17:06:52.857 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: true
//手指离开button  按压返回false
2019-06-27 17:06:52.874 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
2019-06-27 17:06:52.891 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
2019-06-27 17:06:52.908 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
//....下面手指返回button但还是返回false
2019-06-27 17:06:52.925 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false
2019-06-27 17:06:52.942 11293-11293/com.justtest I/liangchaojie: MyLL isPressed: false

真相大白!原来是这个mIgnoreNextUpEvent和press搞的鬼,可是在哪里press无效了呢?

case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    //如果触摸事件超出了view之外
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            //设置按压效果无效
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;

可是如果mIgnoreNextUpEvent被设置成了true是不是下次点击事件就无效了呢???

仔细看源码1.1

  case MotionEvent.ACTION_UP:
    //...
mIgnoreNextUpEvent = false;
break;

最后分发事件结束的时候会重置其状态,所以下次再点击是可以点击的。

至此就把View的分发又重新学习了一遍,感谢阅读到这里,欢迎提出宝贵意见和建议!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值