我重写了onInterceptTouchEvent(ev)方法,但是为什么Action_Move分支没执行?浅析安卓事件分发。

前言

说到安卓的事件分发,大多数人都很难说的很清楚,当然也包括我,之前只是记住了几个结论,什么隧道传递,冒泡处理,什么 dispatchxxx是用来传递事件的,onInterceptxxx是用来拦截事件的,onTouch事件是用来处理事件的,说的门门是道,但是在自己实现逻辑的时候依然会遇到比较懵逼的问题。
现在有做一个需求,是图片的下拉关闭功能,现在很多app都有这个功能,体验性很好,就像下面这张图片
这里写图片描述

这个图片的实现效果将在接下来的博客中进行讲解,这篇博客只讲在开发中遇到的问题。

我复写了OnInterceptTouchEvent方法但是Move分支没执行

讲道理,这个问题的出现,是有点出乎我的意料的。按照示例图来看,这个是ViewPager,但是同时支持了手势的下滑功能。这样,我们就能很自然的想到去拦截move事件,然后判断在手势下滑的时候,将事件拦截,然后交给自己的OnTouchEvent进行处理。
所以我们可以很自然的写下如下代码

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = MotionEvent.ACTION_MASK & ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastDownX = ev.getRawX();
                mLastDownY = ev.getRawY();
                Log.d(TAG,"onInterceptTouchEvent   "+"MotionEvent.ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                float downX = ev.getRawX();
                float downY = ev.getRawY();
                float dx = Math.abs(downX - mLastDownX);
                float dy = Math.abs(downY - mLastDownY);
                if (Math.sqrt(dx * dx + dy * dy) > mTouchSlop && dy > dx) {
                    Log.d(TAG,"onInterceptTouchEvent   "+"MotionEvent.ACTION_MOVE");
                    return onTouchEvent(ev);
                } else {
                    return super.onInterceptTouchEvent(ev);
                }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                break;
        }
        Log.d(TAG,"onInterceptTouchEvent"+super.onInterceptTouchEvent(ev));
        return super.onInterceptTouchEvent(ev);
    }

核心代码 Math.sqrt(dx * dx + dy * dy) > mTouchSlop && dy > dx 滑动的距离大于系统的最小滑动距离切 y轴方向滑动的距离大于x轴方向的距离,将其拦截,交给OnTouch事件处理,所以接下来,我们复写onTouchEvent事件

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastDownX = ev.getRawX();
                mLastDownY = ev.getRawY();
                Log.d(TAG,"onTouchEvent   "+"MotionEvent.ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                float downX = ev.getRawX();
                float downY = ev.getRawY();
                float dx = Math.abs(downX - mLastDownX);
                float dy = Math.abs(downY - mLastDownY);
                Log.d(TAG,"onTouchEvent   "+"MotionEvent.ACTION_MOVE");
                if (Math.sqrt(dx * dx + dy * dy) > mTouchSlop && dy > dx) {
                    // todo 开始图片的缩放动画以及关闭逻辑
                    handleToCloseView();
                    return true;//表明事件被消费了
                } else {
                    return super.onTouchEvent(ev);
                }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                Log.d(TAG,"onTouchEvent   "+"ACTION_CANCEL:\n" +
                        "ACTION_UP:");
                break;
        }
//        Log.d(TAG,"super.onTouchEvent(ev)"+super.onTouchEvent(ev));
        return super.onTouchEvent(ev);
    }

就这样,我们的伪代码就完成了,感觉天意无缝呢。打印下log发现,
这里写图片描述
为啥 onInterceptTouchEvent(ev)中的move事件没执行?我可以保证我的手势操作没问题啊,难道是因为我判断有问题,打断点!!!
结果很失望,不是判断逻辑出错,而是压根就没走进该分支。不对啊,不符合常理,在我印象里,onInterceptxxx是用来拦截事件的,onTouch事件是用来处理事件的,如果我们需要自己处理事件,那我们肯定要复写onInterceptxxx来拦截,现在压根都没走进分支,怪哉,但是OnTouchEvent的Move分支执行了,为啥呢?百度之。
有罗里吧嗦讲一堆事件分发的,看的云里雾里,有直接给答案的,说在子布局里直接设置clickable为true的,那就找最简单的,直接设置clickable为true,实验之:
这里写图片描述
执行了,这下符合逻辑了,但是不科学啊,我不可能在每个子布局里设置这个属性吧,平时用的控件里我也没这么搞过。所以,为啥呢?我们去源码一窥究竟。

入口 dispatchTouchEvent

毕竟事件都是从这里进行分发的,非常幸运的的是ViewPager的源码是很完整的,并没有爆红的情况,我们在里面找该方法,发现并有这个方法,(你在这个源码里打断点你也会发现OnInterceptTouchEvent 的move分支也没执行,当然这个肯定不是代码出错了,至于为什么没执行,和我们遇到的原因一样的。),下面我们开始介绍为什么我们没有接收到Move事件,为什么加了clickable属性,我们就能接到 。所以我们就需要在他的父类ViewGroup里找该方法,源码很晦涩,我只贴主要代码段
这里写图片描述
可以看到 OnInterceptTouchEvent 的执行条件有三个,其中disallowIntercept属性不用管,默认是false;除非你调用了requestDisallowInterceptTouchEvent(true)方法。接下来讲的内容前提条件都是disallowIntercept为false
另外两个是down事件和 target变量,两者是或的关系,这个就解释了,down事件来了 ,一定会执行OnInterceptTouchEvent ,但是move事件来了呢?第一个条件已经不符合,就看第二个是不是mFirstTouchTarget != null,这个是关键点。那么这个值是在哪里赋值的呢?看源码

这里写图片描述
记住这个canceled和intercepted变量,第一个肯定是false了,因为我们整个事件链还没有结束,那么intercepted呢?也为false,因为我们并没有拦截down事件,默认返回时false,(intercepted = onInterceptTouchEvent(ev)),所以该分支一定会走到。接下来这个if分支里会遍历子View,然后不断分发事件。主要源代码
这里写图片描述
其中 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) 方法就是子View的事件分发逻辑,
这里写图片描述
可以看到调用了child.dispatchxxxxx()方法。
那么这个返回值靠什么决定呢?是子view的OnTouchEvent方法返回值.翻源码进去看看.
这里写图片描述
可以看到参数clickable只要为true就会走到该分支,至于里面做了什么,我们完全不用管,因为执行了这么多逻辑后,他还是返回了true,(这个clickable的值是哪里决定的呢,就是setclickable方法,也就是布局里的clickable属性。
然后再返回ViewGroup中的dispatchxxxx方法)所以onTouchEvent就返回了true,所以这个分支就进去了,
这里写图片描述
看标红部分,然后点击源码:
这里写图片描述
mFirstTouchTarget 被赋值,所以不为null,然后下次Move事件来临时候

if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            }

判断逻辑成立,会再次走入onInterceptTouchEvent(ev)方法,所以这下也能接收到Move事件啦~

总结 (disallowIntercept为false情况下)
- 1.OnInterceptTouchEvent 方法不是每次都执行的,但是down事件一定会执行,;
- 2.想要执行 OnInterceptTouchEvent 的move分支,一定需要子View来消费事件,这样父View才能进行下一次的判断要不要拦截其他事件。否则会直接进行子View的分发工作,然后调用子View的ontouchEvent事件,如果子View不进行处理,那么将交给其上级来处理,绕过OnInterceptTouchEvent 方法。见源码
这里写图片描述
那么问题来了

我处理事件分发一定要重写OnInterceptTouchEvent 方法来拦截吗

答:不一定,但是99.9%的情况下你需要重写,1是因为你不确定子View会不会拦截事件,2是为了代码的健壮性。
因为事情的分发是隧道传递,冒泡处理的,假设你自定义ViewGroup,该ViewGroup不被其他控件所嵌套,(这里控件指的是可能存在滑动冲突的控件),那么,要看你自定义的Viewgroup里的子控件有没有消费事件,如果消费了,我们需要再适合的时候进行拦截。否则事件会被子View消费掉。

如果我只写OnTouchEvent事件来处理逻辑,但是没有写拦截的逻辑,那么我的逻辑会受影响吗

答:不一定。跟上面说的一样,假设你的子View里没有进行事件的消费,根据冒泡原则,他会让事件向上处理,这时候假设你只写了这个onTouchEvent事件,然后你的自定义ViewGroup处理了事件并返回了true,你的功能并不会受影响,受影响的是你的健壮性。

为什么会总结出这两点来呢,因为有一段臭名昭著的伪代码,让我误解了,虽然他能恰好的表示dispatchxxx oninterceptxxx ontouchxxx 之间的关系,但是让我误解为只有拦截了事件才能执行OntouchEvent,伪代码如下:

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

        return consume;
    }

在我看来,他应该这么写:

public boolean dispatchTouchEvent(MotionEvent event){

        boolean consume;
        if (evevt.getAction==MotionEvent.ACTION_DOWN||mFirstTarget!=null ){
            if (!requstdisallow){
                consume =onInterceptTouchEvent(event);
            }
        }
        if (consume){
            //不再将事件分发给子View,执行自己的onTouchEvent事件
        }else {
             //如果这时候child.dispatchTouchEvent(event)为true,那么mFirstTarget将被赋值不为null;
             //下次move事件来的时候回走进onInterceptTouchEvent(event)
            return child.dispatchTouchEvent(event) ;
        }
        //执行自己的OnTouch事件(指的是冒泡处理,在不拦截的时候,事件会优先分发给子View,
        //子View若不处理,会交给父View的onTouchEvent处理,如果拦截了,就会走自己的onTouchEvent(event))

        return onTouchEvent(event);
        //......冒泡......//
        //......冒泡......//
        //......冒泡......//
    }

你不拦截事件,自己的ontouchevent事件也会被执行,因为只要你的子View不消耗事件,事件就会被冒泡处理。
你拦截了事件,事件将不分发给子View,同时,自己的OnTouchEvent事件会被执行。

这是我自己的理解,欢迎勘误~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值