美文网首页高级UI
ScrollView的嵌套滑动冲突的解决

ScrollView的嵌套滑动冲突的解决

作者: 波澜步惊 | 来源:发表于2019-06-05 16:40 被阅读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),努力吧,骚年!

    相关文章

      网友评论

        本文标题:ScrollView的嵌套滑动冲突的解决

        本文链接:https://www.haomeiwen.com/subject/yfojxctx.html