高级自定义View系列二;ScrollView的嵌套滑动冲突的解决

作者:波澜步惊
链接:https://www.jianshu.com/p/04ca65b8b200

前言

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

高级自定义view系列文章,都为学员波澜步惊在学习高级进阶课程的过程中所吸收和思考的知识,尤其对于从事Android开发3-5年,处于瓶颈期需要寻找突破和进阶方向的小伙伴们一定有所启发和帮助的

高级自定义View系列思维脑图;

高级自定义View系列二;ScrollView的嵌套滑动冲突的解决_第1张图片

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

正文

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

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

效果

高级自定义View系列二;ScrollView的嵌套滑动冲突的解决_第2张图片

不墨迹,直接给出源码工程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

高级自定义View系列二;ScrollView的嵌套滑动冲突的解决_第3张图片
image

上图中,我们能在重写的onInterceptTouchEvent方法中找到两处return truetrue则拦截或者消费,false则放行或不消费,整个事件分发机制都是这个套路,记住就行了)。
第二处,调用的是父类,也就是FrameLayout的拦截返回值,一般都会返回false放行,不理会即可。
只看第一处,首先,指定拦截ACTION_MOVE事件,并且还有另一个条件。
mIsBeingDragged - 是否正在拖拽。看看这个值什么时候会变成 true,找到下面这个地方(其实还有另一处,在onTouchEvent中,但是现在还没到事件回传的时候,所以不用看)

高级自定义View系列二;ScrollView的嵌套滑动冲突的解决_第4张图片
image

让它变成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方法:

高级自定义View系列二;ScrollView的嵌套滑动冲突的解决_第5张图片
image

可以断定,之前传入的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),努力吧,骚年!

你可能感兴趣的:(高级自定义View系列二;ScrollView的嵌套滑动冲突的解决)