View事件传递机制

理解事件传递的基本逻辑,对于工作过程中解决滑动事件冲突非常有帮助。比如我们此时有一个父控件ViewPager,这个ViewPager其中一个Item是ScrollView,此时会发生什么问题呢?当ViewPager滑动到ScrollView这个条目的时候,再左右滑动,发现ViewPager再也左右滑动不了了。这是为什么呢?我们结合图6一起来分析一下。


  1.我们都知道ViewPager是能够横向滑动的控件,而ScrollView是纵向滑动的控件,当Down事件产生的时候,此时会由ViewPager传递给ScrollView,ViewPager没有对Down事件拦截,ScrollView也不会对这个Down事件进行拦截,所以事件就会传递给ScrollView的孩子,也就是类似于图6中的子View,子View如果没有对Down事件响应,那么最后会到ScrollView中的onTouchEvent,而ScrollView的onTouchEvent对于这个Down事件返回了true,代表ScrollView消费了这个Down事件。


  2.接下来开始滑动手指,产生一系列的Move事件。Move事件也是由ViewPager传递给ScrollView。由于Down事件是被ScrollView的onTouchEvent中消费的,所以Move事件就不会传递给ScrollView的子控件了。一系列的Move事件也是在ScrollView的onTouchEvent中被执行。


  3.最后的Up事件也是由ScrollView中的onTouchEvent消费。


  从上述1至3的步骤中,我们看出来无论是Down事件、Move事件还是Up事件,最后全部都是被ScrollView所消费。从头到尾ViewPager的onTouchEvent都没有得到执行。而ViewPager之所以能够左右滑动,正是因为ViewPager的onTouchEvent里面的代码逻辑产生的效果。ViewPager的onTouchEvent没有执行,这个ViewPager当然就不能够左右滑动了。所以解决上述问题,就是在于如何让ViewPager中的onTouchEvent方法执行。
我们可以自定义一个MyViewPager继承ViewPager,重写onInterceptTouchEvent方法,如果我们在onInterceptTouchEvent方法中直接野蛮地return一个true,此时就代表无论是Down事件、Move事件,还是Up事件,全部都拦截下来了,拦截在MyViewPager中,我们可以认为是图6中的ViewGroupB,既然拦截下来了所有事件,那么所有事件就会传递到MyViewPager的onTouchEvent,所以此时,这个MyViewPager一定可以左右滑动。

  但是,由此会引发另外一个问题,就是这个ScrollView不能上下滑动了。这又是为什么呢?因为ScrollView能够上下滑动的代码逻辑在ScrollView中的onTouchEvent方法内,而此时事件又全部被MyViewPager拦截了下来,ScrollView完全得不到事件,onTouchEvent方法得不到执行,自然不能上下滑动。所以我们需要修改MyViewPager中的onInterceptTouchEvent的逻辑。


  ViewPager只对左右滑动感兴趣,而ScrollView对上下滑动这个动作感兴趣,所以我们只需要在MyViewPager的onInterceptTouchEvent中,根据多个Move事件,判断是左右滑动还是上下滑动,如果是左右滑动,return true将事件拦截下来,如果是上下滑动,return false将事件传递给ScrollView,这样就能解决问题了。
所以,对于Down事件,我们一般都不进行拦截,判断是否拦截得根据一些列的Move事件才能得出具体的条件是否成立。
Cancel事件的产生:

  刚才我们说了事件一般有三个,Down、Move、Up,这三个事件比较好理解。其实还有一种事件就是Cancel事件。它代表什么含义呢?
还是回到图6,如果一个Down事件产生了,这个Down事件从ViewGroupA传递到ViewGroupB,最终到达子View,被子View的onTouchEvent消费,return了true,那么此时Down事件就终止了。接下来后续的Move事件也会从ViewGroupA传递给ViewGroupB,也就是说ViewGroupA和ViewGroupB会比子View更先拿到Move事件,那既然ViewGroupA和ViewGroupB比子View更先拿到Move事件,那么他们当中的任何一个都有可能在某一个Move事件中,把这个Move事件给拦截下来,一旦Move事件被拦截下来了,子View肯定就拿不到这个Move事件了,不过,此时子View会产生一个新的事件,就是Cancel事件。


  所以一个正常的事件序列是 Down→Move→Up,这样才被认为是一个正常的事件序列。如果一个View响应的Down事件,可是却被没有正常结尾,Move事件或者Up事件被拦截了,此时非正常结尾的情况就会给子View产生一个新的事件Cancel。


  子控件可以影响父控件是否拦截的行为
  子控件是可以干预父控件是否拦截事件的结果。通过在子View中dispatchTouchEvent中增加一行代码即可。getParent().requestDisallowInterceptTouchEvent(true);这行代码就可以请求父控件不要拦截事件。


  很多人可能不太明白这句话的意思,既然事件一定是先到达父控件,然后才到达子View,那也就是getParent().requestDisallowInterceptTouchEvent(true);这句话是在父控件是否拦截判断结束之后才调用,怎么能改变父控件是否拦截的结果呢,这里存在一个执行先后顺序的疑惑。
  

  其实是这样的,getParent().requestDisallowInterceptTouchEvent(true);达到的效果不是修改父控件对本次事件是否拦截的结果,而影响的是后续事件。比如子View在Down事件中调用了getParent().requestDisallowInterceptTouchEvent(true);这行代码,那么在后续Move事件、Up事件产生到达父控件的时候,父控件就不会再拦截了。所以getParent().requestDisallowInterceptTouchEvent(true);只会影响Move事件和Up事件,影响不到Down事件。

内容引自如下 

深入浅出解析Android事件传递机制

 

 

事件传递顺序
硬件 -> ViewRootImpl -> Window -> Activity -> PhoneWindow -> DecorView -> VIewGroup -> View
事件传递中主要的三个方法dispatchTouchEvent() onInterceptTouchEvent() 和 onTouchEvent()


 

##View的滑动冲突

当我们内外两层View都可以滑动时候,就会产生滑动冲突。滑动冲突有两种形式,内外两层滑动方向不一致和滑动一致。不管是哪种形式,我们只需要根据我们的逻辑考虑什么时候需要外层View处理滑动,什么时候需要内层View处理滑动即可。

滑动冲突处理的方式有两种,外部拦截法和内部拦截法。

####外部拦截法
外部拦截法是父View根据需要对事件进行拦截。逻辑处理放在父View的onInterceptTouchEvent方法中。我们只需要重写父View的onInterceptTouchEvent方法,并根据逻辑需要做相应的拦截即可。

根据业务逻辑需要,在ACTION_MOVE方法中进行判断,如果需要父View处理则返回true,否则返回false,事件分发给子View去处理。
ACTION_DOWN 一定返回false,不要拦截它,否则根据View事件分发机制,后续ACTION_MOVE 与 ACTION_UP事件都将默认交给父View去处理
ACTION_UP也需要返回false,如果返回true,并且滑动事件交给子View处理,那么子View将接收不到ACTION_UP事件,子View的onClick事件也无法触发
外部拦截法子View不需要做任何处理

####内部拦截法
内部拦截法父View拦截除ACTION_DOWN以外的其它事件。子View在ACTION_DOWN中调用getParent().requestDisallowInterceptTouchEvent(true)方法接管事件并在ACTION_MOVE中根据业务逻辑决定事件是否教给父View处理。如需交给父View处理则调用requestDisallowInterceptTouchEvent(false)方法。内部拦截法不符合事件分发流程,是通过子VIew反向控制父View拦截。伪代码如下:

/**
 * 内部拦截法
 * 父View需拦截除DOWN以外的其他事件
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        super.onInterceptTouchEvent(ev);
        return false;
    } else {
        return true;
    }
}

//子View.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (父容器需要此类点击事件) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
    }

    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

//父View.onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent event) {

    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}
上述代码是内部拦截的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。

父元素要默认拦截除了ACTION_DOWN以外的其他事件
子元素调用parent.requestDisallowInterceptTouchEvent(false/true)来控制父元素是否拦截事件
父元素不能拦截ACTION_DOWN因为它不受FLAG_DISALLOW_INTERCEPT标志位控制,一旦父容器拦截ACTION_DOWN那么所有的事件都不会传递给子View

内容引自如下:

Android View的事件传递及滑动冲突

1.事件传递优先级:onTouchListener.onTouch > onTouchEvent > onClickListener.onClick。
 

标题
标题
标题

 

 



另外参考文章

图解Android View的事件传递

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值