Android 滑动冲突的处理

什么是滑动冲突?


  • 概念:滑动冲突即某些特定的滑动事件被父View拦截导致子View接收不到该事件无法滑动。
  • 基本类型:
    Android滑动冲突基本类型
    其他复杂类型都是由基本类型组成的。

  • 思路
    从滑动冲突的概念可知,只需让子View接收到特定的滑动事件即可解决冲突。
    子View要接收到ACTION_MOVE必须:

    1. ACTION_DOWN:从Android事件分发机制本质是树的深度遍历(图+源码)的结论(即ACTION_DOWN会深度遍历“分发树”并确定“消耗树”,后续同一序列事件都是沿着这一“消耗树”分发(深度遍历,但通常都是线性结构)的,且可被中途拦截但“消耗树”不变。)可知,要让ACTION_DOWN至少能分发到子View并且被子View或更下层的View消耗,其实就是让“消耗树”能够到达子View,这样后续的ACTION_MOVE事件才有机会到达子View。总之,ACTION_DOWN必须被子View或它的下层消耗。

      解决办法:在子View的在onTouchEvent()中消耗ACTION_DOWN。

    2. ACTION_MOVE:父View不拦截子View需要的特定ACTION_MOVE
      解决办法:

      • 外部拦截:重写父View的onInterceptTouchEvent(),不拦截子View需要的特定滑动事件。(“自控”:父View自己控制拦截ACTION_MOVE与否)
      • 内部拦截:在子View的dispatchTouchEvent()中通过调用父View的requestDisallowInterceptTouchEvent()方法阻止父View对子View所需的特定滑动事件的拦截。(“子控”:子View控制父View拦截ACTION_MOVE与否)
        以上两种方法在必要时(若子View的下层View没有消耗ACTION_DOWN事件时)还应重写子View的onTouchEvent()方法,在事件的“结果返回过程”(参考:Android事件分发机制本质是树的深度遍历(图+源码))中消耗ACTION_DOWN事件。

具体解决办法


ACTION_DOWN:子View消耗ACTION_DOWN

若子View的下层View没有消耗ACTION_DOWN事件时,需确保MotionEvent为ACTION_DOWN时onTouchEvent()返回true。
注意:这一点是很多技术博客都没讲的,在处理冲突时要注意。
例子1:ViewPager在onTouchEvent()中ACTION_DOWN时默认返回true。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    ......

    switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN: {
            mScroller.abortAnimation();
            mPopulatePending = false;
            populate();

            // Remember where the motion event started
            mLastMotionX = mInitialMotionX = ev.getX();
            mLastMotionY = mInitialMotionY = ev.getY();
            mActivePointerId = ev.getPointerId(0);
            break;
        }
        ......
    }

    ......

    return true;//ACTION_DOWN时默认消耗
}

经典的PullToRefresh在onTouchEvent()中也是默认消耗ACTION_DOWN,参考:Android-PullToRefresh 之二:详细设计(一、PullToRefresh)中PullToRefreshBase类的onTouchEvent()源码。

ACTION_MOVE:外部拦截

“自控”:父View自己控制拦截ACTION_MOVE与否。
重写父View的onInterceptTouchEvent():

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;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            if (满足父容器的拦截要求) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
    }

    mLastXIntercept = x;
    mLastYIntercept = y;

    return intercepted;
}

缺点:若父View本身已重写onInterceptTouchEvent()且比较复杂则再一次重写必须考虑原来onInterceptTouchEvent()的实现。

ACTION_MOVE:内部拦截

“子控”:子View控制父View拦截ACTION_MOVE与否。

重写子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);//不允许父View拦截后续事件
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (父容器需要此类点击事件) {
                parent.requestDisallowInterceptTouchEvent(false);//允许父View拦截后续事件
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
    }

    mLastX = x;
    mLastY = y;

    return super.dispatchTouchEvent(event);//调用子View原来的dispatchTouchEvent()方法。
}

重写父View的onInterceptTouchEvent()方法:
实际上,父View一般都会自己重写onInterceptTouchEvent(),无需我们再次重写(如ViewPager),若要重写则必须考虑父View原先的onInterceptTouchEvent()。

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

优点:相比外部拦截,这个方法更容易,因为重写dispatchTouchEvent()只是在子View原先的dispatchTouchEvent()的基础上添加控制父View对特定滑动事件拦截的功能,不用考虑子View原先dispatchTouchEvent()的实现,直接调用就好。

无论是“自控”还是“子控”,都应该让ACTION_MOVE的控制逻辑只在第一个ACTION_MOVE事件的时候触发,第一个ACTION_MOVE事件决定交给父View消耗则后续ACTION_MOVE事件都直接交给父View,不要再次调用控制逻辑。
原因:多次触发控制逻辑可能会导致同一系列的不同ACTION_MOVE交给不同的View处理。
注意:这一点是很多技术博客都没讲的,在处理冲突时要注意。

具体例子看ViewPager源码(内部拦截):

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

    ......

    // Nothing more to do here if we have decided whether or not we
    // are dragging.
    if (action != MotionEvent.ACTION_DOWN) {
        if (mIsBeingDragged) {//若ACTION_MOVE已交给ViewPager处理,则后续事件同样交给ViewPager处理,无需再次触发后面的控制逻辑
            if (DEBUG) Log.v(TAG, "Intercept returning true!");
            return true;
        }
        if (mIsUnableToDrag) {//若ACTION_MOVE已交给ViewPager的子View处理,则后续事件同样交给该子View处理,无需再次触发后面的控制逻辑
            if (DEBUG) Log.v(TAG, "Intercept returning false!");
            return false;
        }
    }

    //控制逻辑:判断滑动事件交给谁消耗。
    switch (action) {
        case MotionEvent.ACTION_MOVE: 
        ......
    }

    /*
     * The only time we want to intercept motion events is if we are in the
     * drag mode.
     */
    return mIsBeingDragged;
}

ACTION_MOVE:特殊的内部拦截

当我们不想或者无法重写父View和子View的方法时,还有另一中方式解决滑动冲突。前提是ACTION_DOWN不被子View的下层消耗。

  • 解决办法:为子View设置OnTouchListener,在onTouch()中控制父View对ACTION_MOVE的拦截。
  • 原理:ACTION_DOWN在“结果返回过程”中到达子View时因尚未被消耗从而触发子View调用基类View(子View的父类)的dispatchTouchEvent()方法。而子View设置OnTouchListener后基类View的dispatchTouchEvent()在执行时会调用已设置的OnTouchListener的onTouch()。
    更详细的解释参考:Android事件分发机制本质是树的深度遍历(图+源码)

为子View设置OnTouchListener:

childView.setOnTouchListener(new View.OnTouchListener() {
   @Override
    public boolean onTouch(View v, MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

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

        mLastX = x;
        mLastY = y;

        return false;
    }
});

总结


滑动冲突即某些特定的滑动事件被父View拦截导致子View接收不到该事件无法滑动。解决滑动冲突就是让子View接收到它需要的特定滑动事件。为此,必须:
1. ACTION_DOWN必须被子View或它的下层消耗。
2. 控制父View不拦截子View需要的特定ACTION_MOVE。

猜你喜欢

转载自blog.csdn.net/cheneasternsun/article/details/80686184
今日推荐