美文网首页
NestedScrollView嵌套滑动源码解读

NestedScrollView嵌套滑动源码解读

作者: 今日Android | 来源:发表于2021-05-19 22:03 被阅读0次

    1、前言

    滑动对于android来说,是一个必不可少;它不复杂,大家都知道在onTouchEvent中,让它滑动就完事了,说它复杂,其嵌套处理复杂;在本系列文章,最终是为了熟悉嵌套滑动机制;对于滑动,分为下面几篇文章来完成解读:

    在本章内,本章从两个嵌套的两个视角来分析

    1. 子滑动视图视角:涉及NestedScrollingChild3接口以及NestedScrollingChildHelper辅助类
    2. 父滑动容器视角:涉及NestedScrollingParent3接口以及NestedScrollingParentHelper辅助类

    这篇内容分三个小章节

    1. NestedScrollingChildHelper类
    2. NestedScrollingParentHelper类
    3. 实现处理以及调用时机

    在这里类的解读是必须的,不然只能死记其调用时机,这里是不建议的;下面会贴一部分源码,在源码中会对代码的一些关键进行注释说明

    2、NestedScrollingChildHelper类

    嵌套子视图角色;主要功能

    • 事件是否需要通知
    • 事件通知

    类中如下变量:

        private ViewParent mNestedScrollingParentTouch; // touch事件接力的父容器
        private ViewParent mNestedScrollingParentNonTouch; // 非touch事件接力的父容器
        private final View mView; // 当前容器,也是作为嵌套滑动时孩子角色的容器
        private boolean mIsNestedScrollingEnabled; // 当前容器是否支持嵌套滑动
        private int[] mTempNestedScrollConsumed; // 二维数组,保存x、y消耗的事件长度;减少对象生成的
    复制代码
    

    2.1 实例获取

        public NestedScrollingChildHelper(@NonNull View view) {
            mView = view;
        }
    复制代码
    

    2.2 嵌套滑动支持

    是对嵌套子视图的角色来说的

        public void setNestedScrollingEnabled(boolean enabled) {
            if (mIsNestedScrollingEnabled) {
                ViewCompat.stopNestedScroll(mView); // 兼容模式调用
            }
            mIsNestedScrollingEnabled = enabled;
        }
    
        public boolean isNestedScrollingEnabled() {
            return mIsNestedScrollingEnabled;
        }
    复制代码
    

    2.3 嵌套滑动相关方法

    要支持嵌套滑动,那么必须有多个支持嵌套滑动的容器;作为子视图,其需要有通知的一套,因此方法有:

    • 父容器的查找、判断
    • 通知开始、过程以及结束

    2.3.1 嵌套父容器的查找

    成员变量mNestedScrollingParentTouch、mNestedScrollingParentNonTouch为父容器缓存变量;其直接设置和获取方法如下

      private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
            switch (type) {
                case TYPE_TOUCH:
                    return mNestedScrollingParentTouch;
                case TYPE_NON_TOUCH:
                    return mNestedScrollingParentNonTouch;
            }
            return null;
        }
    
        private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
            switch (type) {
                case TYPE_TOUCH:
                    mNestedScrollingParentTouch = p;
                    break;
                case TYPE_NON_TOUCH:
                    mNestedScrollingParentNonTouch = p;
                    break;
            }
        }
    复制代码
    

    2.3.2 嵌套父容器的支持判断

        public boolean hasNestedScrollingParent() {
            return hasNestedScrollingParent(TYPE_TOUCH);
        }
    
        public boolean hasNestedScrollingParent(@NestedScrollType int type) {
            return getNestedScrollingParentForType(type) != null;
        }
    复制代码
    

    2.3.3 滑动开始通知

        public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
            if (hasNestedScrollingParent(type)) {
                return true;
            }
            if (isNestedScrollingEnabled()) { // 孩子视图支持嵌套滑动,只有支持才会继续执行
                ViewParent p = mView.getParent();
                View child = mView;
                while (p != null) { // 查找的不仅仅直接父容器
                    // 兼容调用,父容器是否可以作为嵌套父容器角色
                    if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                        setNestedScrollingParentForType(type, p); // 这里进行了缓存
                        // 兼容调用,父容器
                        ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                        return true;
                    }
                    if (p instanceof View) {
                        child = (View) p;
                    }
                    p = p.getParent();
                }
            }
            return false;
        }
    复制代码
    

    父容器的查找,采取了延时策略,在进行事件时,才进行查询,并且在查询到了,进行支持;所以可以这样理解:

    1. onStartNestedScroll:是父容器接受事件通知方法,其结果表示是否可以作为嵌套滑动的父容器角色
    2. onNestedScrollAccepted:不是必调用,调用了表明嵌套父容器角色支持view的后续嵌套处理

    2.3.4 手指滑动通知

    滑动时通知,分为滑动前和滑动后;使嵌套滑动处理更灵活 滑动前通知

        public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
                @Nullable int[] offsetInWindow) {
            return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH);
        }
    
        public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
                @Nullable int[] offsetInWindow, @NestedScrollType int type) {
            if (isNestedScrollingEnabled()) {
                final ViewParent parent = getNestedScrollingParentForType(type);
                if (parent == null) {
                    return false;
                }
    
                if (dx != 0 || dy != 0) {
                    int startX = 0;
                    int startY = 0;
                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        startX = offsetInWindow[0];
                        startY = offsetInWindow[1];
                    }
    
                    if (consumed == null) {
                        consumed = getTempNestedScrollConsumed();
                    }
                    consumed[0] = 0;
                    consumed[1] = 0;
                    ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
    
                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        offsetInWindow[0] -= startX;
                        offsetInWindow[1] -= startY;
                    }
                    return consumed[0] != 0 || consumed[1] != 0;
                } else if (offsetInWindow != null) {
                    offsetInWindow[0] = 0;
                    offsetInWindow[1] = 0;
                }
            }
            return false;
        }
    复制代码
    

    其中两个二维数组作为结果回传;通过父容器的onNestedPreScroll方法进行处理并把滑动处理详情放入两个二维数组中,常用的详情为消耗长度情况;返回结果表示滑动前是否处理

    滑动后通知

        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
            return dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                    offsetInWindow, TYPE_TOUCH, null);
        }
    
        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
            return dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                    offsetInWindow, type, null);
        }
    
        public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type,
                @Nullable int[] consumed) {
            dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                    offsetInWindow, type, consumed);
        }
    
        private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
                @NestedScrollType int type, @Nullable int[] consumed) {
            if (isNestedScrollingEnabled()) {
                final ViewParent parent = getNestedScrollingParentForType(type);
                if (parent == null) {
                    return false;
                }
    
                if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                    int startX = 0;
                    int startY = 0;
                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        startX = offsetInWindow[0];
                        startY = offsetInWindow[1];
                    }
    
                    if (consumed == null) {
                        consumed = getTempNestedScrollConsumed();
                        consumed[0] = 0;
                        consumed[1] = 0;
                    }
    
                    ViewParentCompat.onNestedScroll(parent, mView,
                            dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
    
                    if (offsetInWindow != null) {
                        mView.getLocationInWindow(offsetInWindow);
                        offsetInWindow[0] -= startX;
                        offsetInWindow[1] -= startY;
                    }
                    return true;
                } else if (offsetInWindow != null) {
                    offsetInWindow[0] = 0;
                    offsetInWindow[1] = 0;
                }
            }
            return false;
        }
    复制代码
    

    其中两个二维数组作为结果回传;通过父容器的onNestedScroll方法进行处理并把滑动处理详情放入两个二维数组中,常用的详情为消耗长度情况;返回结果表示滑动前是否处理

    2.3.5 滑翔通知

    滑翔也有两个时机

    滑翔前

       public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
            if (isNestedScrollingEnabled()) {
                ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
                if (parent != null) {
                    return ViewParentCompat.onNestedPreFling(parent, mView, velocityX,
                            velocityY);
                }
            }
            return false;
        }
    复制代码
    

    返回结果表明父容器的是否处理滑翔;父容器是通过onNestedPreFling进行处理

    滑翔后

      public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
            if (isNestedScrollingEnabled()) {
                ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
                if (parent != null) {
                    return ViewParentCompat.onNestedFling(parent, mView, velocityX,
                            velocityY, consumed);
                }
            }
            return false;
        }
    复制代码
    

    返回结果表明父容器的是否处理滑翔;父容器是通过onNestedFling进行处理

    滑翔是一个互斥处理的过程,而滑动是一个接力的过程

    2.3.6 滑动结束通知

        public void stopNestedScroll() {
            stopNestedScroll(TYPE_TOUCH);
        }
    
        public void stopNestedScroll(@NestedScrollType int type) {
            ViewParent parent = getNestedScrollingParentForType(type);
            if (parent != null) {
                // 通知嵌套父容器,滑动结束
                ViewParentCompat.onStopNestedScroll(parent, mView, type);
                setNestedScrollingParentForType(type, null); // 清理父容器引用
            }
        }
    复制代码
    

    3、NestedScrollingParentHelper类

    作为嵌套滑动的父容器角色,其只有接受通知时处理即可,情况没有子视图角色那么复杂;而辅助类里仅仅是对滑动方向做了声明周期处理;

    成员变量

        private int mNestedScrollAxesTouch; // Touch事件时,接受处理时,事件的滑动方法
        private int mNestedScrollAxesNonTouch;  // 非Touch事件时,接受处理时,事件的滑动方法
    复制代码
    

    3.1 滑动方向获取

        public int getNestedScrollAxes() {
            return mNestedScrollAxesTouch | mNestedScrollAxesNonTouch;
        }
    复制代码
    

    3.2 滑动方向设置

        public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
                @ScrollAxis int axes) {
            onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);
        }
    
        public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
                @ScrollAxis int axes, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_NON_TOUCH) {
                mNestedScrollAxesNonTouch = axes;
            } else {
                mNestedScrollAxesTouch = axes;
            }
        }
    复制代码
    

    3.3 滑动方向重置

       public void onStopNestedScroll(@NonNull View target) {
            onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
        }
    
        public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
            if (type == ViewCompat.TYPE_NON_TOUCH) {
                mNestedScrollAxesNonTouch = ViewGroup.SCROLL_AXIS_NONE;
            } else {
                mNestedScrollAxesTouch = ViewGroup.SCROLL_AXIS_NONE;
            }
        }
    复制代码
    

    4、嵌套实现机制

    作为一是具有兼容性实现的嵌套滑动容器,它必须实现下面接口

    • 滑动容器接口ScrollingView
    • 嵌套滑动父容器接口NestedScrollingParent3
    • 嵌套滑动子视图接口NestedScrollingChild3

    嵌套接口,可以根据容器角色选择实现;方法实现需要利用辅助类

    从上面对两个辅助类解读;对他们已经实现的功能做了归纳

    1. 嵌套是否支持
    2. 嵌套通知
    3. 嵌套滑动方向

    也就是作为子视图角色的实现方法基本使用辅助类即可,而嵌套父容器角色需要我们增加实现逻辑;需要实现从功能上划分:

    1. 作为嵌套子视图设置,
    2. 作为嵌套父容器的实现
    3. 滑动接力处理,以及滑翔处理

    4.1 嵌套子视图支持

    构造器中进行setNestedScrollingEnabled(true)方法进行设置

    setNestedScrollingEnabled方法

        public void setNestedScrollingEnabled(boolean enabled) {
            mChildHelper.setNestedScrollingEnabled(enabled);
        }
    复制代码
    

    4.2 嵌套父容器的支持

        public boolean onStartNestedScroll(
                @NonNull View child, @NonNull View target, int nestedScrollAxes) {
            return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
        }
    
        public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
                int type) {
            return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
        }
    复制代码
    

    可滑动方向判断进而决定是否支持的;支持时的处理如下

        public void onNestedScrollAccepted(
                @NonNull View child, @NonNull View target, int nestedScrollAxes) {
            onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
        }
    
        public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
                int type) {
            mParentHelper.onNestedScrollAccepted(child, target, axes, type);
            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
        }
    复制代码
    

    其还是一个子视图角色,所以,其需要继续传递这个滑动开始的信号;可见嵌套默认处理中:其实是一个嵌套滑动容器链表,中间也可能存在滑动容器(不支持嵌套),链表组后一个容器的‘父’容器也还可能是嵌套滑动;这些情况造成的一个原因是同时是父容器还是子视图才会继续分发;这个链头容器必定是个嵌套子视图角色,中间即是子视图角色也是父容器角色,链尾容器必定是个嵌套父容器角色

    时机

    在down事件中,调用startNestedScroll方法

    4.3 利用辅助类重写

    下面方法利用了辅助类直接重写

    • 嵌套父容器存在判断:hasNestedScrollingParent
    • 子视图是否支持嵌套滑动:setNestedScrollingEnabled、isNestedScrollingEnabled
    • 开始通知:startNestedScroll
    • 滑动分发:dispatchNestedPreScroll、dispatchNestedScroll
    • 滑翔分发:dispatchNestedPreFling、dispatchNestedFling
    • 结束通知:stopNestedScroll

    参数中涉及到滑动类型时,均采用ViewCompat.TYPE_TOUCH作为默认类型

    4.4 滑动接力处理

        public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
            onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
        }
    
        public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
                int type) {
            dispatchNestedPreScroll(dx, dy, consumed, null, type);
        }
    复制代码
    

    其作为父容器,本身对事件并没有处理,而是作为子视图继续分发下去;时机move事件中嵌套子视图处理滑动之前

        public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed) {
            onNestedScrollInternal(dyUnconsumed, ViewCompat.TYPE_TOUCH, null);
        }
    
        private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
            final int oldScrollY = getScrollY();
            scrollBy(0, dyUnconsumed);
            final int myConsumed = getScrollY() - oldScrollY;
    
            if (consumed != null) {
                consumed[1] += myConsumed;
            }
            final int myUnconsumed = dyUnconsumed - myConsumed;
    
            mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
        }
    复制代码
    

    父容器首先处理了滑动,然后把处理后的情况继续传递;时机move事件,嵌套子视图处理之后

    4.5 滑翔互斥处理

        public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
            return dispatchNestedPreFling(velocityX, velocityY);
        }
    
        public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
            return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
        }
    复制代码
    

    不进行处理,而是做为嵌套子视图继续分发;时机up事件,拦截时,嵌套子视图处理之前

        public boolean onNestedFling(
                @NonNull View target, float velocityX, float velocityY, boolean consumed) {
            if (!consumed) {
                dispatchNestedFling(0, velocityY, true);
                fling((int) velocityY);
                return true;
            }
            return false;
        }
    复制代码
    

    如果接受到通知时,未处理,则进行处理;并做为嵌套子view继续通知处理;时机up事件,拦截时,嵌套子视图处理之后

    4.6 滑动结束

        public void onStopNestedScroll(@NonNull View target) {
            onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
        }
        public void onStopNestedScroll(@NonNull View target, int type) {
            mParentHelper.onStopNestedScroll(target, type);
            stopNestedScroll(type);
        }
        public void stopNestedScroll(int type) {
            mChildHelper.stopNestedScroll(type);
        }
    复制代码
    

    由于还是嵌套子视图角色,还需要通知其处理的嵌套父容器结束;时机up、cancel事件时

    4.7 嵌套子视图优先处理

    android中,从容器的默认拦截机制来看,父容器优先拦截;但是嵌套时做了额外判断,

    滑动事件拦截中是这样判断的

    yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0)
    复制代码
    

    滑动的坐标轴为0,也就是既不是x轴、也不是y轴;这说明,它作为嵌套父容器时,没有嵌套子容器传递给它;

    另外如果滑动已经被拦截处理,则不希望其它进行再次拦截;这时由于嵌套拦截体系已经提供了交互的方法,如果不这样处理,就会导致和默认的事件机制冲突;因此,如果有这种情况,那就把重写父容器,让其支持嵌套滑动吧

    5 小结

    总的来说,嵌套滑动呢,它抽象了接口和辅助类,来帮助开发者进行实现;其中实现的核心思触发点

    1. 嵌套的组织关系
    2. 嵌套的互相通知处理
    3. 自己处于角色中,是否需要处理以及如何处理

    Jetpack compose在开源项目:https://github.com/Android-Alvin/Android-LearningNotes中已收录,里面还包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中...

    相关文章

      网友评论

          本文标题:NestedScrollView嵌套滑动源码解读

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