android事件分发机制、解决滑动冲突思路

导言

Android中的滑动冲突很常见,例如ScrollView/ListView,ViewPager/ViewPager,相信各位或多或少都了解Android事件分发机制,以及滑动冲突产生的原理。网上相关的文章也很多,并且都讲解的很详细。但那毕竟是别人的成果,我觉得有必要通过一篇文章来记录自己的理解。

大纲

我将从下面几个方面来理解事件分发和解决滑动冲突:

  1. 理解四个方法
  2. Android事件分发机制
  3. 解决滑动冲的思路
  4. 一个滑动冲突场景
  5. 总结
  6. 参考文章

1.理解四个方法

讲到Android事件分发机制和解决滑动冲突,就离不开这四个方法:

  • dispatchTouchEvent(MotionEvent ev)
  • onInterceptTouchEvent(MotionEvent ev)
  • onTouchEvent(MotionEvent ev)
  • requestDisallowInterceptTouchEvent(boolean disallowIntercept)

大概介绍一下前三个方法的关系:

/**
 * 伪代码
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onInterceptTouchEvent(ev)) {
        return onTouchEvent(ev);
    }
    return child.dispatchTouchEvent(ev);;
}

dispatchTouchEvent()

正如其方法名,该方法是用来传递事件的,传递的顺序是Activity -> ViewGroup -> View
只要事件传递到当前view,就一定会调用该方法,返回结果表示是否消费该事件:

  • true : 消费该事件,不再继续传递
  • false : 事件不再向下传递,并且把事件交给parent处理
  • super : 事件继续 向下传递

onInterceptTouchEvent()

是否拦截事件(不再向下传递),该方法只存在于ViewGroup,一旦拦截,那么当前整个事件序列不会再调用该方法,后续事件都交给当前ViewGroup处理。返回结果表示是否拦截:

  • true : 拦截,事件不再向下传递
  • false/super : 不拦截,事件继续传递

onTouchEvent()

该方法用来处理事件,处理的顺序是View -> ViewGroup -> Activity,返回结果表示是否处理事件:

  • true : 处理事件,不再向下传递
  • false : 不处理事件,同一个事件序列里面,该View无法收到后续事件
  • super : 交给上层View处理

requestDisallowInterceptTouchEvent()

该方法是在子view中请求父view不要拦截事件,参数disallowIntercept的值表示:

  • true : 请求所有父view不要拦截事件,即当前事件序列不走父view的onInterceptTouchEvent()方法,直接向下传递
  • false : 请求所有父View拦截事件,即子view不需要处理该事件,直接交给父view处理

该方法的作用下面还会详细介绍。

2.Android事件分发机制

在介绍事件分发机制之前,先介绍一下事件序列(上文有提到过):

 

事件序列.png

注:一般情况下,事件列都是以DOWN事件开始,UP事件结束,中间有很多MOVE事件

接下来,用一张图看明白事件传递的过程中的方法调用:

 

事件分发图.png

假如事件传递不中断的话,方法调用的整个流程如下图:

 

事件方法调用顺序.png

如果仔细看上面两张图,大家基本就能明白事件的传递流程了,下面我用文字描述一下整个流程:

  1. 事件从Activity的dispatchTouchEvent开始传递,传递给ViewGroup
  2. 如果ViewGroup的onInterceptTouchEvent不拦截事件,则继续向下面(ViewGroup或者View)传递,如果拦截了,则事件直接交给ViewGroup的onTouchEvent处理
  3. 当事件传递到了View,View就会调用onTouchEvent处理事件,正常情况下,还会把事件交给ViewGroup的onTouchEvent处理
  4. ViewGroup处理事件之后,正常情况下,又会交给Activity的onTouchEvent处理。

这里再把几个需要注意的点提一下:

  • 如果ViewGroup的onInterceptTouchEvent方法拦截了事件,事件序列的后续事件不会再调用次方法,也不会向下传递,都直接交给该ViewGroup处理
  • 如果View没有对ACTION_DOWN事件进行消费,事件序列的后续事件都不会传递过来了

3.滑动冲突解决方案

面对滑动冲突,我们可以有2种解决思路:

  1. 外部拦截法:是指我们可以重写parent的onInterceptTouchEvent方法,判断当前的事件是否需要拦截,伪代码如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean isIntercept = false;
    switch(event.getAction) {
        case MotionEvent.ACTION_DOWN:
            isIntercept = false;
            //todo 记录点击初始位置
            break;
        case MotionEvent.ACTION_MOVE:
            if (子控件不需要处理滑动事件) {
                isIntercept = true;
            } else {
                isIntercept = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            isIntercept = false;
            break;
    }
    super.onInterceptTouchEvent(event);
    return isIntercept;
}

在这里,down事件必须返回false,不然事件无法传递到子view,后续事件序列都会交给parent处理。而up事件也需要返回false,因为up事件对parent来说没有什么意义,其次若子view处理事件,却没有收到up事件会让子view的onClick事件无法触发。

  1. 内部拦截法:是指我们可以重写child的dispatchTouchEvent方法,判断是否需要让parent拦截事件,伪代码如下:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch(event.getAction) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptToucehEvent(true);
            //todo 记录点击初始位置
            break;
        case MotionEvent.ACTION_MOVE:
            if (子控件需要处理滑动事件) {
                getParent().requestDisallowInterceptToucehEvent(true);
            } else {
                getParent().requestDisallowInterceptToucehEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.onDispatchTouchEvent(event);
}

如果parent.requestDisallowInterceptTouchEvent(true)传入参数为true,则parent就不会执行onInterceptTouchEvent方法,直接把事件交给子view处理。
requestDisallowInterceptTouchEvent()方法的逻辑:parent的dispatchTouchEvent()每次down事件,都会把它置为false,即拦截事件,走parent的onInterceptTouchEvent()方法,而在onInterceptTouchEvent()方法中,有一个属性mIsBeingDragged,当dy(滚动距离)>mTouchSlop的时候置为true,down事件和dy<mTouchSlop时为false,最后onInterceptTouchEvent()方法返回mIsBeingDragged,说明即使parent拦截了事件,但滚动距离比较小的时候,事件仍可以传递给子view,子view可以在onTouchEvent方法中调用parent.requestDisallowInterceptTouchEvent()方法

一个滑动冲突的场景

这里举一个很简单的例子
场景:ViewPager嵌套ViewPager的滑动冲突
解决思路:内部拦截法,当子ViewPager的position处于0且dx>0,或者子ViewPager的position处于adapter.count-1且dx<0时,把事件交给父ViewPager,其他时候都是子ViewPager处理即可
代码实现:

package study.self.zf.scrollconflict.widget;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class ChildViewPager extends ViewPager {
    private int mStartX;
    private int mStartY;

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mStartX = (int) ev.getX();
                mStartY = (int) ev.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = (int) getX() - mStartX;
                int dy = (int) getY() - mStartY;
                if (Math.abs(dx) > Math.abs(dy)) {
                    int position = getCurrentItem();
                    int allCount = getAdapter().getCount();
                    boolean isInterceptByParent = (position == 0 && dx > 0) || ((position == allCount -1) && dx < 0);
                    getParent().requestDisallowInterceptTouchEvent(!isInterceptByParent);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

总结

从以上的讲解可以看出来,滑动冲突并不难,而且思路也很简单,无非就是从dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()、requestDisallowInterceptTouchEvent()方法入手,分析什么时候parent处理事件,什么时候子view处理事件即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值