Android 如何解决自定义View的事件冲突

在项目里经常会有一些需求 是我们原生控件做不出的效果,所以只能我们自己去绘制,但同时自定义view,就不会自己去化解一些冲突,所以这个时候需要我们做一些代码防范。

1.滑动冲突:一下是解决滑动冲突的几点思路:

Android 如何解决自定义View的事件冲突_第1张图片

就简单举个例子 :场景如下

拿一个电商APP为例,最外一层是一个ScrollView,包裹着recycleView ,是一个商品列表,下面是一个WebView,(需求流程是当recycleView滑到底部时候才全体滑动),往往大家都会有这么一个问题,就是滑到中间或者下方,就出现互动冲突整体跟着滑动,recyclerView还没有滑动完毕,就跟着一起滑动了。

而滑动场景往往是一下三种:

Android 如何解决自定义View的事件冲突_第2张图片

 

1、外部滑动和内部滑动方向不一致

       对于图一场景,平时工作中最常见的使用大概是外层为PageView,内层为一个Fragment+ListView/RecyclerView了。当然控件PageView和RecyclerView对事件冲突做了处理的,所以平时使用这两个控件的时候不会感受到滑动冲突的存在。如果是ScrollView+GridView等这类组合,就需要解决冲突了。

  2、外部滑动和内部滑动方向一致

       同样,这种场景除了图二中的内外都是上下滑动的情况外,还包括内外到时左右滑动的场景了。ScollView(垂直滚动)+ListView的组合就是比较常见的场景。

三、滑动冲突三种场景的处理思路

  1、内外滑动方向不一致时处理思路

       这一类场景其实比较容易分析,因为外层和内层滑动的方向不一致,所以根据手势的动向来确定把事件给谁。默认情况下,当点击内层控件时,事件会先一层层从外层传到内层,由内层来处理。这里以外层为左右滑动,内层为上下滑动为例。当判定手势的滑动为左右时,需要外层来消费事件,所以外层将事件拦截,即在外层的onInterceptTouchEvent中检测为ACTION_MOVE时返回true;而如果判定手势的滑动为上下时,需要内层来消费事件,外层不需要拦截,事件会传递到内层来处理(具体的代码实现,在后面会详细列出)。这样就通过判断滑动的方向来决定事件的处理对象,从而解决滑动冲突的问题。

       那么,如何来判定手势的滑动方向呢?最常用的办法就是比较水平和竖直方向上的位移值来判断。 MotionEvent事件包含了事件的坐标,只要记录一次移动事件的起点和终点坐标,如下图所示,通过比较在水平方向的位移|dx|和|dy|的大小,来决定滑动的方向:|dy|>|dx|,本次移动的方向认为是竖直方向;反之,则认为是水平方向。当然,还可以通过夹角α的大小、斜率、速率等方式来作为判断条件。

  2、内外滑动方向一致时处理思路

       这种场景要比上面一种复杂一些,因为滑动方向一致,所以无法通过上述的方式来判断将事件交给谁处理。在这种情况下,往往需要根据业务的需要来判定谁来处理事件。比如竖直方向的ScrollView嵌套ListView的场景下,手指在ListView上上下滑动时:当ListView滑动到顶部且手势向下时,显然ListView不能再向下滑动了,这种情况下事件需要被外层控件拦截,由ScrollView来消费;当ListView滑动到底部且手势向上时,显然ListView也不能再向上滑动了,这种情况下事件也需要被外层控件拦截,由ScrollView来消费;其它情况下,ScrollView就不能再拦截了,滑动事件就需要由ListView来消费了,即此时上下滑动时,滑动的是ListView,而不是ScrollView。后面会以这为案例进行编码实现。

  3、多层滑动嵌套时处理思路

       场景3看起来比较复杂,但前面也说过了,也是由前面两种场景嵌套形成的。所以在处理场景的处理方式,就是将其拆分为简单的场景,然后按照前面的场景分析方式来处理。

 

四、滑动冲突的两种解决套路

 

  1、外部拦截法

       顾名思义,就是在外部滑动控件中处理拦截逻辑。这需要外部控件重写父类的onInterceptTouchEvent方法,在其中判断什么时候需要拦截事件由自身处理,什么时候需要放行将事件传给内层控件处理,内部控件不需要做任何处理。这个“套路”的伪代码表示所示:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if(父容器需要自己处理改事件){
                intercepted = true;
            }else {
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
            default:
            break;
    }
    return intercepted;
}

  2、内部拦截法

       顾名思义,就是将事件是否需要拦截的逻辑,放到内层控件中来处理。这种方式需要结合requestDisllowInterceptTouchEvent(boolean),在内层控件的重写方法dispatchTouchEvent中,根据逻辑来决定外层控件何时需要拦截事件,何时需要放行。伪代码如下:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if (父容器需要处理改事件) {
                //允许外层控件拦截事件
                getParent().requestDisallowInterceptTouchEvent(false);
            } else {
                //需要内部控件处理该事件,不允许上层viewGroup拦截
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    return super.dispatchTouchEvent(ev);
}

//外层添加:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}

ACTION_DOWN事件仍然不能拦截,ACTION_DOWN时会初始化一些状态和标志位等变量,requestDisllowInterceptTouchEvent(boolean)作用会失效。这里再顺便强调一下,不明白的可以去上一篇文章中阅读这部分内容。 

       这种方式比“外部拦截法”稍微复杂一些,所以一般推荐使用前者。同前者一样,这也是一个套路用法,无论是之前提到的何种场景,只要根据实际判断条件修改上述if语句即可。

代码示例:

1.外部拦截问题示例参考方案:

2.内部拦截问题示例参考方案:

 

你可能感兴趣的:(Android 如何解决自定义View的事件冲突)