高级UI

ScrollView的嵌套滑动冲突的解决

2019-06-05  本文已影响9人  波澜步惊

前言

做程序开发,基础很重要。同样是拧螺丝人家拧出来的可以经久不坏,你拧出来的遇到点风浪就开始颤抖,可见基本功的重要性。此系列,专门收录一些看似基础,但是没那么简单的小细节,同时提供权威解决方案。喜欢的同志们点个赞就是对我最大的鼓励!先行谢过!

网上可能有一些其他文章,提供了解决方案,但是要么就是没有提供可运行demo,要么就是demo不够纯粹,让人探索起来受到其他代码因素的影响,无法专注于当前这个知识点(比如,我只是想了解Activity的生命周期,你把生命周期探究的过程混入到一个很复杂的大杂烩Demo中,让人一眼就没有了阅读Demo代码的欲望),所以我觉得有必要做一个专题,用最纯粹的方式展示一个的解决方案.

正文

记得有一次要使用多个ScrollView嵌套的时候,需要同时让两层ScrollView的滑动都能生效。但是,当我直接套了两层ScrollView之后,发现内层的滑动完全无效了。

研究一番之后发现解决方案其实非常简单。

效果

多层ScrollView嵌套.gif

不墨迹,直接给出源码工程github.

关键代码

android的事件分发滑动冲突的基础知识,这里不再赘述。
两种解决方案:
1,自定义外层ScrollView的拦截行为. 重写onInterceptTouchEvent,直接返回false,外层不再拦截事件。

public class OutsideScrollView extends ScrollView {

    public OutsideScrollView(Context context) {
        this(context, null);
    }

    public OutsideScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public OutsideScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

}

2、自定义内层 ScrollView的拦截行为,调用 getParent().requestDisallowInterceptTouchEvent(true);不允许外层对它的事件进行拦截.


public class InsideScrollView extends ScrollView {
    public InsideScrollView(Context context) {
        this(context, null);
    }

    public InsideScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public InsideScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //如果我不允许外部拦截我呢?
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        getParent().requestDisallowInterceptTouchEvent(true);
        return super.onInterceptTouchEvent(ev);
    }

}

原理

先来解决 第一个疑问 不进行上面的处理时内部的ScrollView滑动不了呢?

解读一下ScrollView的源码,发现,它重写了 ViewonInterceptTouchEventonTouchEvent

image.png
上图中,我们能在重写的onInterceptTouchEvent方法中找到两处return truetrue则拦截或者消费,false则放行或不消费,整个事件分发机制都是这个套路,记住就行了)。
第二处,调用的是父类,也就是FrameLayout的拦截返回值,一般都会返回false放行,不理会即可。
只看第一处,首先,指定拦截ACTION_MOVE事件,并且还有另一个条件。
mIsBeingDragged - 是否正在拖拽。看看这个值什么时候会变成 true,找到下面这个地方(其实还有另一处,在onTouchEvent中,但是现在还没到事件回传的时候,所以不用看)
image.png
让它变成true判定条件为:
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
yDiff > mTouchSlop的意思是,Y轴上的滑动距离,要大于设备规定的最小滑动距离.
(getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0 的意思是,此视图组的嵌套滚动的当前轴是否是纵向(看了注释之后理解的,这里不能debug很蛋疼). getNestedScrollAxes()的值应该是0,因为搜索了全文,发现针对mNestedScrollAxes值的变动,在类内部就只有赋值为0的情况,而&与运算,只要有0,就可以断言整个都是0了,所以==0,成立。
两者都是true,则进入if。 进入之后:mIsBeingDragged = true; 便会执行。
当第一个move执行之后,mIsBeingDragged 已经是true。当第二个move来的时候,ScrollView便会阻拦后面所有的move。 这就是内层ScrollView不能滑动的原因。

第二个疑问:为什么自定义外层 scrollView,重写 onInterceptTouchEvent 直接 return false之后,内层就能正常滑动呢,而且手指在内层滑动时,外层是不动的?

重写了onInterceptTouchEvent 直接 return false,那原本scrollViewonInterceptTouchEvent过程则不会执行。现在,所有的事件直接透传,那么内层ScrollView就可以收到事件,自然就有了滑动效果。但是,当手指在内层滑动时,外层不受影响。这是为何。
答案在 ScrollViewonTouchEvent方法内(代码太长,我就不贴全部了)

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();

        MotionEvent vtev = MotionEvent.obtain(ev);

        final int actionMasked = ev.getActionMasked();

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        vtev.offsetLocation(0, mNestedYOffset);

        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                if (getChildCount() == 0) {
                    return false;
                }
                .... 省略N 行代码
                break;
            }
            ... 省略N 行代码
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();
        return true;
    }

很明确,ScrollViewonTouchEvent,消费掉了除DOWN之外的所有事件。所以外层ScrollView收不到move,自然就没有任何反应。

第三个疑问:内层拦截 getParent().requestDisallowInterceptTouchEvent(true)到底做了什么,让外层无法拦截事件?

先看getParent, 众所周知,View不是一个独立个体,它是一个树形结构,有一个parent节点,也有Nchild节点。这个getParent实际上就是得到自己的父View
看看ViewGrouprequestDisallowInterceptTouchEvent

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

可以看到入参:disallowIntercept的值,改变了全局变量mGroupFlags 的值。并且,这个方法将disallowIntercept的值向父View传递。
全局变量mGroupFlags 什么时候用到呢?
进入ViewGroupdispatchTouchEvent方法:

image.png
可以断定,之前传入的disallowIntercept 入参值,一定可以影响到这里的局部变量boolean disallowIntercept的值,并且如果之前传入true,这里就会得到true你问我为什么会断定?因为这是在书上看到的。。。具体过程涉及到数字的位运算,贼复杂,在这里说不清楚,以后做专题的时候再讲吧).
如果之前传入的是true,那么这里就会执行else 中的 intercepted = false; 也就是,不会执行这个
intercepted = onInterceptTouchEvent(ev); 明白了吧? 如果内层调用了requestDisallowInterceptTouchEvent(true),在父viewdispatchTouchEvent中,就不会执行onInterceptTouchEvent.

值得一提的是,requestDisallowInterceptTouchEvent(true) 方法内部,调用了mParent.requestDisallowInterceptTouchEvent(disallowIntercept);,让这个bool值会一直向上传递,也就是说,如果一个子view调用了这个方法,那么它的父,父的父。。。节点,都不会拦截它的事件。

结语

阅读源码是一个痛苦的过程,随时随地会发现自己的知识盲区。但是,不读源码,就不知道源码的深浅,就无法进阶成高级工(super)程(ma)师(nong),努力吧,骚年!

上一篇下一篇

猜你喜欢

热点阅读