NestedScrollingParent

一篇文章让你轻松弄懂NestedScrollingParent & NestedScrollingChild

NestedScrollView 从源码到实战..

手把手实现ScrollView+ViewPager+RecyclerView常规嵌套首页布局

unknown_filename.6|600

谁实现 NestedScrollingChild,谁实现 NestedScrollingParent ?

在实际项目中,我们往往会遇到这样一种需求,当 ViewA 还显示的时候,往上滑动到 viewA 不可见时,才开始滑动 viewB,又或者向下滑动到 viewB 不能滑动时,才开始向上滑动 viewC. 如果列表滑动、上拉加载和下拉刷新的 view 都封装成一个组件的话,那滑动逻辑就是刚刚这样。

而这其中列表就要实现 nestedScrollingChild, 最外层的 Container 实现 nestedScrollingParent. 如果最外层的 Container 希望在其它布局中仍然能够将滑动事件继续往上冒泡,那么 container 在实现 nestedScrollingParent 的同时也要实现 nestedScrollingChild。如下示意图所示。

image.png
所以这个问题的答案:

触发滑动的组件或者接受到滑动事件且需要继续往上传递的是nestedScrollingChild.

nestedScrollingChild的父布局,且需要消费传递的滑动事件就是nestedScrollingParent.

滑动事件如何在二者之间传递和消费的?

分辨API

一个分辨是childparentapi的一个小诀窍,因为child是产生滑动的造势者,所以它的api都是以直接的动词开头,而parent的滑动响应是child通知parent的,所以都是以监听on开头,这样就记住了。
parent —-> onXXXX()
child —–> verbXXXX()

方法执行流程规范

滑动产生时,由child主动通知,parent被动接收判断处理。这里的child和parent不必是直接父子关系,child会向上遍历parent。
child.startNestedScroll -> parent.onStartNestedScroll -> parent.onNestedScrollAccepted ->
child.dispatchNestedPreScroll -> parent.onNestedPreScroll -> child.dispatchNestedScroll ->
parent.onNestedScroll -> child.stopNestedScroll ->parent.onStopNestedScroll

在onTouchEvent方法中,首先在DOWN时通过通知parent对滑动进行判断响应。之后在ACTION_MOVE过程中,计算滑动偏移量,优先交由parent进行消耗处理,若有parent接收处理,则在parent滑动后,减去parent消耗的偏移量,在交给自身或子view进行剩余偏移量的滑动。若自身或子view滑动后还有剩余的偏移量,则再交由parent处理。最后在UP/CANCEL通知parent滑动结束。

NestedScrollingChild

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 通知开始滑动,会回调父容器的onStartNestedScroll方法。
*/
boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);

/**
* 通知停止滑动,会回调父容器的onStopNestedScroll方法。
*/
void stopNestedScroll(@NestedScrollType int type);

/**
* 查询是否有父容器支持指定类型的嵌套滑动。
*/
boolean hasNestedScrollingParent(@NestedScrollType int type);

/**
* 在子视图处理滑动前,先将滚动偏移量传递给父容器。
*/
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type);

/**
* 子视图处理滑动后,再将剩余的滚动偏移量传递给父容器。
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type);

NestedScrollingChild 和 NestedScrollingChild2的区别:
image.png

  • NestedScrollType.TYPE_TOUCH 表示正常的滑动
  • NestedScrollType.TYPE_NON_TOUCH 表示在滑动过程中迅速点击屏幕,终止滑动

NestedScrollingChild3 和 NestedScrollingChild2 的区别:
image.png

NestedScrollingParent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 子视图触发滑动时会回调该方法,父容器在该方法中根据子view、滑动方向、触摸类型等判断自己是否支持接收,
* 若接收返回true,否则返回false。(可由NestedScrollingChild2的startNestedScroll方法触发)
*/
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type);

/**
* onStartNestedScroll返回true后会回调该方法,可在此方法中做一些初始配置操作。
*/
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type);

/**
* 开始滑动时,子视图会优先回调该方法。父容器可以处理自己的滚动操作,之后将剩余的滚动偏移量
* 传回给子视图。(可由NestedScrollingChild2的dispatchNestedPreScroll方法触发)
*/
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type);

/**
* 子视图处理完剩余的滚动偏移量后,若还有剩余,则将剩余的滚动偏移量再通过该回调传给
* 父容器处理。(可由NestedScrollingChild2的dispatchNestedScroll方法触发)
*/
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);

/**
* 当滑动结束时,回调该方法。(可由NestedScrollingChild2的stopNestedScroll方法触发)
*/
void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);

tips: NestedScrollingParent2 和 NestedScrollingParent3 改动和 NestedScrollingChlid2/NestedScrollingChlid3 一样

部分参数含义说明

  • child:表示包含target的当前容器的直接子view。
  • target:表示调用startNestedScroll触发onStartNestedScroll回调的那个子view。
  • axes:表示即将滑动的坐标轴方向,通过位运算求出方向。
  • type:表示触摸类型,有TYPE_TOUCH(用户触摸)、TYPE_NON_TOUCH(惯性滑动)两种类型。
  • dx:水平滑动偏移量。<0表示手指向右划,>0则相反。
  • dy:垂直滑动偏移量。<0表示手指向下划,>0则相反。
  • consumed:保存父容器滑动消耗的偏移量(索引0存x轴偏移,1存y轴偏移)。在父容器滑动后,子view会将原偏移量减去consumed中的值得到剩余偏移量,再进行自身的滚动处理。
  • dxConsumed:子view消耗的水平偏移量。
  • dyConsumed:子view消耗的垂直偏移量。
  • dxUnconsumed:子view滑动后还剩下的水平偏移量。
  • dyUnconsumed:子view滑动后还剩下的垂直偏移量。
    注意:若有用户触摸滑动到惯性滑动,会走两遍方法执行流程,即不同type各触发一次流程。

NestedScrollView源码分析

为什么 NestedScrollView 只能添加 1个 ChildView

image.png

可以从 NestedScrollView#addView(View child, ViewGroup.LayoutParams params) 中看出,在添加第二个 View 的时候,直接就报错了,报错信息为:
image.png

NestedScrollView的事件分发流程

事件分发主要分为:

  • onInterceptTouchEvent
  • onTouchEvent
    • ACTION_DOWM
    • ACTION_MOVE
    • ACTION_UP / ACTION_CANCEL

RecycleView

==事件都是从子view开始的==

onTouchEvent#ACTION_DOWN事件

在 TouchEvent.DOWN 事件中通过NestedScrollingChildHelper调用 NestedScrollingChild#startNestedScroll()方法,那么NestedScrollingChildHelper就会通过么ViewParentCompat调用到 NestedScrollingParent#onStartNestedScroll()上,parentView 用来判断是否需要嵌套滚动,如果需要的话,返回 true,则立即调用到NestedScrollingParent#onNestedScrollAccepted上 完成最初的事件传递
image.png

onTouchEvent#ACTION_MOVE事件

ACTION_MOVE : 小手指滑动位移为:dy
–> childHelper.dispatchNestedPreScroll(dy)
–> parent.onNestedPreScroll(dy), consumedY = parent.onNestedPreScroll(dy)
–> dy' = dy - consumeY recyclerView.scrollByInternal(dy') unconsumeY = dy' - recyclerView.scrollByInternal(dy')
–> parent.startNestedScroll(unconsumeY) 将未消耗的滑动位移继续移交给自己的parent

onTouchEvent#ACTION_UP事件

先是child执行fling方法,也就是当手松开时仍然有速度,那么会执行一段惯性滑动,而在这惯性滑动中, 这里就很奇妙了,先是通过dispatchNestedPreFling()将滑动速度传递给parent, 如果parent不消耗的话,再次通过dispatchNestedFlingparent传递,只是这次的传递会带上child自己是否有能力消费惯性滑动,最后不管parent有没有消费,child也就是recyclerview都会执行自己的fling.也就是:

mViewFlinger.fling(velocityX, velocityY);

ACTION_UP

–> childHelper.dispatchNestedPreFling
–> parent.onNestedPreFling
–> childHelper.dispatchNestedFling
–> parent.onNestedFling
–> child.fling
–> childHelper.stopNestedScroll
–> parent.onStopNestedScroll

这样,我们整个nestedScrollingChildnestedScrollingParent之间的丝丝缕缕都讲解完了。


NestedScrollingParent
http://peiniwan.github.io/2024/04/738bc280a4e1.html
作者
六月的雨
发布于
2024年4月6日
许可协议