View的事件分发机制与滑动冲突的解决

        View的事件分发机制可以说是View最核心的知识点之一,而View滑动冲突的解决,理论基础就是基于View的分发机制,所以,掌握好View的分发机制可以帮助我们更好的解决滑动冲突以及更好的处理一些点击事件,下面就来带大家了解一下View的事件分发机制:

首先View的分发机制分发的是什么?

分发的就是MotionEvent事件,那什么是MotionEvent事件?
这里我们只需要了解一下MotionEvent的典型的事件类型:

ACTION_DOWN--手指刚触碰到屏幕
ACTION_MOVE--手指在屏幕滑动
ACTION_UP--手指离开手机屏幕

且通过MotionEvent的对象可以获取到点击事件发生的x,y坐标
getX()/getY() 指的是相对于父控件的左上角的x,y坐标
getRawX()/getRawY()指的是相对于手机屏幕左上角的x,y坐标

那点击事件(MotionEvent)到底是怎样被传递的?

下面是一个点击事件的完整传递过程:

Activity ——> Window(Activity.getWindow())——>DecorView(Activity.getWindow().getDecorView())——>ViewGroup(我们setContentView的那个顶级ViewGroup)——>......——>View
 

点击事件的传递最重要的就是下面三个方法:

(1)public boolean dispathTouchEvent(MotionEvent ev)

*用来进行事件的分发,如果事件能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent方法和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

  (2)public boolean onInterceptTouchEvent(MontionEvent event)  

*在上一个方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么此后的同一事件序列当中,此方法不会再被调用,返回结果表示是否拦截当前事件。

  (3)public boolean onTouchEvent(MontionEvent event)

*在第一个方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

View对事件其实就是一个从父容器到子容器逐层向下分发的过程,如果某一层View拦截了点击事件,那么此后一个事件序列的所有事件都交给他处理,一个时间序列指手指从触屏到离开所产生的所有点击事件的序列。

若一个View的onTouchEvent返回了false那么它父容器的onTouchEvent将会被调用,即如果子View不能消耗这个点击事件,那么又会把此事件向上委托,由他的上一层View来处理。

笼统的来说View的事件分发就是点击事件逐层向下传递,若有View拦截了点击事件那么此点击事件就交由它来处理,若他不能消耗掉此点击事件(即onTouchEvent返回了false)则同一事件序列后面的事件又会向上传递,交由上层View来处理。

好了,到这大家应该对View的事件分发有了简单的认识,有了这基础知识,相信大家对滑动冲突的解决已经有了自己的想法,下面

滑动冲突的产生主要有下面三个场景:

第一种:外层View只支持左右滑动,内层View只支持上下滑动。

第二种:外层View跟内层View均只支持左右滑动。

第三张:第一和第二场景的嵌套,看似复杂,其实滑动冲突解决的思想大同小异。

        当前解决滑动冲突主要采用的就是对事件的选择拦截,即在拦截方法中加入逻辑判断是否需要此事件,需要则拦截,不需要则向下传递,由此衍生两种拦截方法,外部拦截法,内部拦截法

(1)外部拦截法

        所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的 onInterceptTouchEvent 方法,在内部做相应的拦截即可,这种方法的伪代码如下所示:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        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;
        }
        return intercepted;
    }

 

        上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需要做修改并且也不能修改。这里对上述代码在描述一下,在 onInterceptTouchEvent 方法中,首先是 ACTION_DOWN 这个事件,父容器必须返回 false,即不拦截 ACTION_DOWN 事件,这是因为一旦父容器拦截了 ACTION_DOWN,那么后续的 ACTION_MOVE 和 ACTION_UP 事件都会直接交由父容器处理,这个时候事件就没法再传递给子元素了;其次是 ACTION_MOVE 事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回  true,否则就返回  false ;最后是 ACTION_UP 事件,这里必须要返回 false,因为 ACTION_UP 事件本身没有太多意义。

        考虑一种情况,假设事件交由子元素处理,如果父容器在 ACTION_UP 时返回了 true,就会导致子元素无法接收到 ACTION_UP事件,这个时候子元素中的 onClick 事情就无法触发,但是父元素比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而 ACTION_UP 作为最后一个事件也必定可以传递给父容器,即便父容器的 onInterceptTouchEvent 方法在 ACTION_UP 时返回了 false。

(2)内部拦截法

         内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接 消耗掉,否则就交由父容器进行处理,这种方法和 Android 中的事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,使用起来外部拦截法稍显复杂。它的伪代码如下,我们需要重写子元素的 dispatchTouchEvent 方法:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此类点击事件) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }
 

        上述代码是内部拦截法的典型代码,当面对不同的滑动策略是只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。除了子元素需要做处理以外,父元素也要默认拦截除了 ACTION_DOWN 以外的其他事件,这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false) 方法时,父元素才能继续拦截所需的事件。

       为什么父元素不能拦截 ACTION_DOWN 事件呢? 那是因为 ACTION_DOWN 事件并不受 FLAG_DISALLOW_INTERCEPT 这个标记位的控制,所以一旦父容器拦截了 ACTION_DOWN 事件,那么所有的事件都无法传递到子元素中去,这样内部拦截法就无法起作用了。父元素所做的修改如下所示:


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
    }
 

了解了这两种拦截方法我们就可以针对场景一场景二和场景三来编写解决滑动冲突的代码,下面以场景一为例:

         对于场景1,它的处理规则就是:当用户左右滑动是,需要让外部的 View 拦截点击事件,当用户上下滑动时,需要让内部 View 拦截点击事件。这个时候我们就可以根据它们的特征来解决滑动冲突,具体来说是:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件,如下图所示,根据滑动过程中两个点之间的坐标就可以得出到底是水平滑动还是竖直滑动。如何根据坐标来得到滑动的方向呢?这个很简单,有很多可以参考,比如可以依据滑动路径和水平方向所形成的夹角,也可以根据水平方向和竖直方向上的距离差来判断,某些特殊时候还可以依据水平和竖直方向的速度差来做判断。这里我们可以通过水平和竖直方向的距离差来判断,比如竖直方向滑动的距离大就判断为竖直滑动,否则判断为水平滑动。根据这个规则就可以进行下一步的解决方法的制定了。


外部拦截的核心代码:

public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            //返回 Scroller 是否已完成滚动
            if (!mScroller.isFinished()) {
                //停止动画。与forceFinished(boolean)相反,Scroller滚动到最终x与y位置时中止动画。
                mScroller.abortAnimation();
                intercepted = true;
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            // 水平滑动距离差 大于 竖直滑动距离差,拦截当前点击事件
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }

        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
        }

        Log.d(TAG, "intercepted=" + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }

        通过上述代码可以看到,在外部View我们在他的onInterceptTouchEvent对ACTION_MOVE是否拦截做了判断,需要则拦截,不需要则向下传递。这便是利用外部拦截解决场景一的滑动冲突,内部拦截里面的逻辑判断大同小异,只是需要将逻辑判断放到内部的dispatchTouchEvent中,如果需要则自己处理,不需要则调用getParent().requestDisallowInterceptTouchEvent(false);来让外部处理此点击事件。

由于篇幅有限,我就不再一一赘述其他场景的滑动冲突的解决方法了,其实核心思想大家现在应该已经清楚了,剩下的就留给聪明的你自己研究探索了O(∩_∩)O。

发布了9 篇原创文章 · 获赞 11 · 访问量 261

猜你喜欢

转载自blog.csdn.net/Healer_LU/article/details/103576548