NestedScrollView嵌套滑动源码解读 一时失言乱红尘 2023-01-22 12:50 5阅读 0赞 ## 1、前言 ## 滑动对于android来说,是一个必不可少;它不复杂,大家都知道在onTouchEvent中,让它滑动就完事了,说它复杂,其嵌套处理复杂;在本系列文章,最终是为了熟悉嵌套滑动机制;对于滑动,分为下面几篇文章来完成解读: * [滑动基础][Link 1] * [ScrollView滑动源码解读][ScrollView] * NestedScrollView嵌套滑动源码解读 * CoordinatorLayout-AppBarLayout-CollapsingToolbarLayout复杂滑动逻辑源码解读 在本章内,本章从两个嵌套的两个视角来分析 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][https_github.com_Android-Alvin_Android-LearningNotes]中已收录,里面还包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中… > ![b3c42cfe887c05b53fc90c26fef83903.png][] [Link 1]: https://juejin.cn/user/2383396940559191/posts [ScrollView]: https://juejin.cn/post/6962423025799168007 [https_github.com_Android-Alvin_Android-LearningNotes]: https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FAndroid-Alvin%2FByteDance-Android-Architecture%2Fblob%2Fmaster%2F%25E5%25AD%2597%25E8%258A%2582%25E8%25B7%25B3%25E5%258A%25A8%25E7%25A7%25BB%25E5%258A%25A8%25E6%259E%25B6%25E6%259E%2584%25E5%25B8%2588%25E5%25AD%25A6%25E4%25B9%25A0%25E7%25AC%2594%25E8%25AE%25B0.md [b3c42cfe887c05b53fc90c26fef83903.png]: /images/20221020/842a84aedd234b91b5ff8f8feaf2dca2.png
还没有评论,来说两句吧...