滑动冲突解决-联动子View

在做SuperRefreshLayout这个下拉刷新的控件时,发现了一个问题,这也是很多下拉刷新控件都会有的问题.

如果用户在顶部下拉时触发了控件的下拉效果,此时用户不放手向上滑动,会取消该控件的下拉效果.但是**用户继续向上滑动时,子View却不会向下滚动了.**一定要用户抬起手重新向下滚动才可以.

界面状态分别为:

普通状态(图1) 拉动中(图2) 刷新中(图3)

当用户从普通状态(图1)向下拉动进入拉动中状态(图2),用户向上滑动取消拉动动作,恢复普通状态(图1),此时继续向上滑动,界面并不会跟随用户手指滚动.

问题分析:

外层刷新控件称为RefreshLayout, 内部子控件称为子View.

  1. RefreshLayout在由图1向下滑动至图2的过程中,其实是RefreshLayout拦截了当前的Touch事件,此时:

    1. RefreshLayout的onInterceptTouchEvent()方法返回true,后续的Touch事件都会交给RefreshLayout的onTouchEvent()处理.
    2. 子View的dispatchTouchEvent()方法会接收到Action_cancel事件,表示当前触摸事件已经结束,不会再处理接收到的除Action_down之外的事件.
    3. RefreshLayout的onTouchEvent()方法消费Touch事件,界面逐渐变为图2.
  2. RefreshLayout由图2向上滑动恢复图1状态后并继续向上滑动时,界面无法向下滑动,因为:

    1. RefreshLayout拦截了Touch事件,子View无法接收.
    2. RefreshLayout的onTouchEvent()方法返回false(因为当子View可以向下滑动时不需要处理).

解决思路:

我们的最终目标就是把RefreshLayout”错误”拦截的Touch事件交给子View来处理.

问题 原因 解决
怎么判断是”错误”的拦截 有需要拦截的Touch事件(如滑动到顶部后继续下拉) 在拦截后记录标志位, 在RefreshLayout的onTouchEvent()方法中不该处理(return false)的情况中判断该标志位
为什么子View无法接收Touch事件 在一开始滚动的过程中RefreshLayout的onInterceptTouchEvent()方法返回true,拦截了所有的Touch事件 事件传递机制是系统设定的,不好更改.但可以在RefreshLayout的onTouchEvent()方法中调用子View的dispatchTouchEvent()方法,让子View接受并处理该Touch事件
怎么让子View能够处理该Touch事件 RefreshLayout拦截Touch事件后,子View会接收到Action_cancel事件,后续事件无法处理 判断是否为拦截后的第一个Touch事件,是的话先模拟一个Action_down事件并传递给子View,使子View认为这是一个新的Touch事件序列.

解决代码:

以下代码为简略版:

//首先定义一个变量,检测是否是同一点击事件序列中第一个拦截后应该处理的move事件
private boolean isFirstMoveAfterIntercept = false;

 //在dispatchTouchEvent方法中初始化
 @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        isFirstMoveAfterIntercept = true;
    }
    if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
        isFirstMoveAfterIntercept = false;
    }
    return super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    ......
    //mIsBeingDragged表示被拦截了
    if (mIsBeingDragged) {
        //此时是错误的拦截(不需要拦截的情况)
        if (originalDragPercent < 0) {
            //判断是否是拦截后的第一个Move事件
            if (isFirstMoveAfterIntercept) {
                // 先为子View模拟一个Action_Down事件,使其认为这是一个新的Touch事件序列
                MotionEvent event = MotionEvent.obtain(ev);
                event.setAction(MotionEvent.ACTION_DOWN);
                mTarget.dispatchTouchEvent(event);
                isFirstMoveAfterIntercept = false;
            }
            //将Move事件传递到子View
            mTarget.dispatchTouchEvent(ev);
            return false;
        }
        ...
    }
    ....
}