NestedScrollingParent
一篇文章让你轻松弄懂NestedScrollingParent & NestedScrollingChild
手把手实现ScrollView+ViewPager+RecyclerView常规嵌套首页布局
谁实现 NestedScrollingChild,谁实现 NestedScrollingParent ?
在实际项目中,我们往往会遇到这样一种需求,当 ViewA
还显示的时候,往上滑动到 viewA
不可见时,才开始滑动 viewB
,又或者向下滑动到 viewB
不能滑动时,才开始向上滑动 viewC
. 如果列表滑动、上拉加载和下拉刷新的 view
都封装成一个组件的话,那滑动逻辑就是刚刚这样。
而这其中列表就要实现 nestedScrollingChild
, 最外层的 Container
实现 nestedScrollingParent
. 如果最外层的 Container
希望在其它布局中仍然能够将滑动事件继续往上冒泡,那么 container
在实现 nestedScrollingParent
的同时也要实现 nestedScrollingChild
。如下示意图所示。
所以这个问题的答案:
触发滑动的组件或者接受到滑动事件且需要继续往上传递的是nestedScrollingChild
.
是nestedScrollingChild
的父布局,且需要消费传递的滑动事件就是nestedScrollingParent
.
滑动事件如何在二者之间传递和消费的?
分辨API
一个分辨是child
和parent
的api
的一个小诀窍,因为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 |
|
NestedScrollingChild 和 NestedScrollingChild2的区别:
- NestedScrollType.TYPE_TOUCH 表示正常的滑动
- NestedScrollType.TYPE_NON_TOUCH 表示在滑动过程中迅速点击屏幕,终止滑动
NestedScrollingChild3 和 NestedScrollingChild2 的区别:
NestedScrollingParent
1 |
|
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
可以从 NestedScrollView#addView(View child, ViewGroup.LayoutParams params)
中看出,在添加第二个 View 的时候,直接就报错了,报错信息为:
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
上 完成最初的事件传递
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
不消耗的话,再次通过dispatchNestedFling
向parent
传递,只是这次的传递会带上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
这样,我们整个nestedScrollingChild
和nestedScrollingParent
之间的丝丝缕缕都讲解完了。