NestedScrolling

NestedScrolling作用

解决嵌套滑动

比ViewGroup事件分发处理的优越性

事件分发:子View首先得到事件处理权,处理过程中,父View可以对其拦截,但是拦截了以后就无法再还给子View(本次手势内)。

NestedScrolling:内部View在滚动的时候,首先将dx,dy交给NestedScrollingParent,NestedScrollingParent可对其进行部分消耗,剩余的部分还给内部View。

事件分发,对于拦截相当于一锤子买卖,只要拦截了,当前手势接下来的事件都会交给Parent(拦截者)来处理。使用ViewGroup事件分发处理实现某些效果,需要手动调用分发事件、手动发出事件。

NestedScrolling原理

嵌套滑动存在两个角色:子View和父View(注:一个View可以同时扮演2个角色,子View并不一定是父View的直接子View)

  1. 子View在收到DOWN事件时,会找到自己祖上中最近的能与自己匹配的父View,与它进行绑定并关闭它的事件拦截机制
  2. 然后子View会在接下来的MOVE事件中判定出用户触发了滑动手势,并把事件流拦截下来给自己消费(注:在NestedScrollView、RecyclerView这类经典实现中,只要用户手指一按下,就会拦截事件流。)
  3. 消费事件流时,对于每一次MOVE事件增加的滑动距离: 
    子View并不是直接自己消费,而是先把它交给父View,让父View可以在子View之前消费滑动 
    如果父View没有消费或是没有消费完,子View再自己消费剩下的滑动 
    如果子View自己还是没有消费完这个滑动,会再把剩下的滑动交给父View消费 
    最后如果滑动还有剩余,子View可以做最终的处理

对每一次MOVE事件传递来的滑动,都使用「parent -> child -> parent -> child」机制进行消费,让子View在消费滑动时与父View配合更加细致、紧密和灵活

NestedScrolling具体接口和调用时机

NestedScrolling 接口分为两个部分:NestedScrollingParent 及 NestedScrollingChild。

NestedScrollingParent

 
  1. public interface NestedScrollingParent {
  2. /**
  3. * 开始NestedScroll时调用
  4. * 对子孙View开始滑动请求的回应(NestedScrollingChild.startNestedScroll)
  5. * 返回true就意味着后面可以接受到NestedScroll事件,否则就无法接收。
  6. *
  7. * @param child 该view的直接子view(包含发起请求的子孙View)
  8. * @param target 发出NestedScroll事件的子孙View,和child不一定是同一个
  9. * @param nestedScrollAxes 滑动的方向,为ViewCompat#SCROLL_AXIS_HORIZONTAL或者ViewCompat#SCROLL_AXIS_VERTICAL,亦或两者都有。
  10. * @return 返回true代表要消耗这个NestedScroll事件,否则就是false。
  11. * */
  12. public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
  13.  
  14. /**
  15. * 在onStartNestedScroll之后调用
  16. * 对开始滑动响应的回调(onStartNestedScroll返回true之后会有此回调产生)
  17. * 参数意义同上
  18. * 使NestedScrollingParent有做滑动初始化工作的时机
  19. * */
  20. public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
  21.  
  22. /**
  23. * 终止nestedscroll的回调(NestedScrollingChild调用stopNestedScroll)
  24. *
  25. * @param target 发出NestedScroll事件的子孙View
  26. * */
  27. public void onStopNestedScroll(View target);
  28.  
  29. /**
  30. * 在target每次滑动之前会调用这个方法
  31. * 在NestedScrollingChild处理滑动之前,预处理此滑动
  32. *
  33. * consumed是一个长度为2的数组。
  34. * 第0位时我们在x方向消耗的滑动距离
  35. * 第1位是我们在y方向上消耗的滑动距离
  36. * 子view会根据这个和dx/dy来计算余下的滑动量,来决定自己是否还要进行剩下的滑动。
  37. * 比如我们使consumed[1] = dy,那么子view在y方向上就不会滑动。
  38. *
  39. * @param target 发出NestedScroll事件的子孙View
  40. * @param dx 这次滑动事件在x方向上滑动的距离
  41. * @param dy 这次滑动事件在y方向上滑动的距离
  42. * @param consumed 回填参数,填入此次预处理消耗掉的滑动距离
  43.  
  44. * */
  45. public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
  46.  
  47. /**
  48. * 在target滑不动的时候会调用这个方法
  49. * 处理NestedScrollingChild未消耗完的滑动距离。
  50. * 如果目标view可以一直滑动,那么这个方法就不会被调用
  51. *
  52. * @param target 发出NestedScroll事件的子孙View
  53. * @param dxConsumed 已消耗的x轴滑动距离
  54. * @param dxUnconsumed 未消耗的x轴滑动距离
  55. * @param dyConsumed 已消耗的y轴滑动距离
  56. * @param dyUnconsumed 未消耗的y轴滑动距离
  57. *
  58. * */
  59. public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
  60. int dxUnconsumed, int dyUnconsumed);
  61.  
  62. /**
  63. * 在target判断为fling并且执行fling之前调用,我们可以通过返回true来拦截目标的fling,这样它就不会执行滑动。
  64. * @param target 发出请求的子孙View
  65. * @param velocityX 在x方向的起始速度
  66. * @param velocityY 在y方向的起始速度
  67. * @return 我们是否消耗此次fling,返回true代表拦截,返回false,目标view就进行正常的fling
  68. * */
  69. public boolean onNestedPreFling(View target, float velocityX, float velocityY);
  70.  
  71. /**
  72. * 在target进行fling后调用。注意这个方法并不是像onNestedScroll在子view滑不动之后调用,而是紧跟着onNestedPreFling后会被调用。因此对于它的使用场景一般比较少。
  73. *
  74. * @param target 目标view
  75. * @param velocityX 在x方向的速度,注意这是fling的起始速度,并不是目标在滑不动时停止时刻的速度,它和onNestedPreFling中的velocityX是一样的。
  76. * @param velocityY 在y方向的速度,注意这是fling的起始速度,并不是目标在滑不动时停止时刻的速度,它和onNestedPreFling中的velocityY是一样的。
  77. * @param consumed 目标view是否消耗了此次fling
  78. * @return 本view是否消耗了这次fling,return true会拦截掉内部View的事件
  79. * */
  80. public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
  81.  
  82. /**
  83. * 获取滑动方向
  84. * SCROLL_AXIS_NONE = 0 没有滑动方向
  85. * SCROLL_AXIS_HORIZONTAL = 1 << 0 横向滑动
  86. * SCROLL_AXIS_VERTICAL = 1 << 1 纵向滑动
  87. * @return 滑动方向
  88. */
  89. public int getNestedScrollAxes();
  90. }

NestedScrollingChild

 
  1. public interface NestedScrollingChild {
  2. /**
  3. * 设置当前View是否启用NestedScroll特性,一般设置为true
  4. * @param enabled 是否启用
  5. */
  6. void setNestedScrollingEnabled(boolean enabled);
  7.  
  8. /**
  9. * 判断当前View是否启用了NestedScroll特性
  10. * @return 是否启用
  11. */
  12. boolean isNestedScrollingEnabled();
  13.  
  14. /**
  15. * 在axes轴上发起NestedScroll开始操作
  16. * @param axes 滑动方向
  17. * @return 是否有NestedScrollingParent接受此次滑动请求,如果不接受返回false,后续的嵌套滑动都将失效
  18. */
  19. boolean startNestedScroll(@ViewCompat.ScrollAxis int axes);
  20.  
  21. /**
  22. * 终止NestedScroll
  23. */
  24. void stopNestedScroll();
  25.  
  26. /**
  27. * 当前是否有NestedScrollingParent接受了此次滑动请求
  28. * @return 是否有NestedScrollingParent接受此次滑动请求
  29. */
  30. boolean hasNestedScrollingParent();
  31.  
  32. /**
  33. * NestedScroll滑动操作中,在自己开始滑动处理之前,分配预处理操作(一般为询问NestedScrollingParent是否消耗部分滑动距离)
  34. * @param dx 当前这一步滑动的x轴总距离(相对于上一次事件,而不是相对于DOWN事件)
  35. * @param dy 当前这一步滑动的y轴总距离
  36. * @param consumed 预处理操作消耗掉的距离(此为输出参数,consumed[0]为预处理操作消耗掉的x轴距离,consumed[1]为预处理操作消耗掉的y轴距离)
  37. * @param offsetInWindow 可选参数,可以为null。为输出参数,获取预处理操作使当前view的位置偏移(offsetInWindow[0]和offsetInWindow[1]分别为x轴和y轴偏移)
  38. * @return 预处理操作是否消耗了部分或全部滑动距离
  39. */
  40. boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
  41. @Nullable int[] offsetInWindow);
  42.  
  43. /**
  44. * 在当前View处理了滑动之后继续分配滑动操作 (一般在自己处理滑动之后,给NestedScrollingParent机会处理剩余的滑动距离)
  45. * @param dxConsumed 已经消耗了的x轴滑动距离
  46. * @param dyConsumed 已经消耗了的y轴滑动距离
  47. * @param dxUnconsumed 未消耗的x轴滑动距离
  48. * @param dyUnconsumed 未消耗的y轴滑动距离
  49. * @param offsetInWindow 可选参数,可以为null。为输出参数,获取预处理操作使当前view的位置偏移(offsetInWindow[0]和offsetInWindow[1]分别为x轴和y轴偏移)
  50. * @return 预处理操作是否消耗了部分或全部滑动距离
  51. */
  52. boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
  53. int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
  54.  
  55. /**
  56. * 在当前NestedScrollingChild处理fling事件之前进行预处理(一般询问NestedScrollingParent是否处理消耗此次fling)
  57. * @param velocityX x轴速度
  58. * @param velocityY y轴速度
  59. * @return 预处理是否处理消耗了此次fling
  60. */
  61. boolean dispatchNestedPreFling(float velocityX, float velocityY);
  62.  
  63. /**
  64. * 分配fling操作
  65. * @param velocityX x轴方向速度
  66. * @param velocityY y轴方向速度
  67. * @param consumed 当前NestedScrollingChild是否处理了此次fling
  68. * @return NestedScrollingParent是否处理了此次fling
  69. */
  70. boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
  71. }

NestedScrollingParent与NestedScrollingChild接口对应关系

NestedScrollingChild NestedScrollingParent
startNestedScroll onStartNestedScroll
  onNestedScrollAccepted
stopNestedScroll onStopNestedScroll
dispatchNestedScroll onNestedScroll
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedFling onNestedFling
dispatchNestedPreFling onNestedPreFling
  getNestedScrollAxes

调用

针对一次滑动操作,子View接口调用顺序为: 
startNestedScroll -> dispatchNestedPreScroll -> dispatchNestedScroll -> stopNestedScroll

  1. 子View在处理MotionEvent时,决定发起滑动请求,调用startNestedScroll
  2. 调用startNestedScroll会向上逐层遍历父View,调用其onStartNestedScroll接口。如果接口返回true,则此View为与此次NestedScroll联动的父View,中断遍历;返回false则继续向上层遍历直到根View。如果遍历到根View还没找到联动的父View,则后续滑动不可用联动。如果找到了,则进入第3步。(注:以下父View指的是onStartNestedScroll接口返回true的View)
  3. 子View调用父View的onNestedScrollAccepted接口。
  4. 父View的onNestedScrollAccepted接口被调用,做一些滑动初始工作。
  5. 子View探测到用户交互产生了滑动距离,调用父View的onNestedPreScroll接口。
  6. 父View的onNestedPreScroll接口被调用,预处理此次滑动,消耗部分滑动距离(或者不消耗)。
  7. 子View处理剩余的滑动距离。
  8. 如果子View没有处理完剩下的滑动距离,则调用dispatchNestedScroll。
  9. 父View的onNestedScroll被调用,自行决定是否继续处理剩下的滑动距离。
  10. 交互上的滑动终止,子View调用stopNestedScroll。
  11. 父View的onStopNestedScroll被调用。
子View 父View 备注
startNestedScroll onStartNestedScroll、onNestedScrollAccepted 如果onStartNestedScroll返回true,则调用onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll  
dispatchNestedScroll onNestedScroll  
stopNestedScroll onStopNestedScroll  

版本变化

第一个版本,2014年9月

在 Android 5.0 / API 21 (2014.9) 时, Google 第一次加入了 NestedScrolling 机制。

重构第一个版本,2015年4月

因为第一个版本的 NestedScrolling 机制是加在 framework 层的 View 和 ViewGroup 中,所以能享受到嵌套滑动效果的只能是Android 5.0的系统,也就是当时最新的系统。 所以在当时 NestedScrolling 机制基本没有怎么被使用。 
Google重构出来两个接口(NestedScrollingChild、NestedScrollingParent)两个 Helper (NestedScrollingChildHelper、NestedScrollingParentHelper)外加一个开箱即用的NestedScrollView,在 Revision 22.1.0 (2015.4) 到来之际,把它们一块加入了v4 support library。Revision 22.2.0 (2015.5)时,Google又隆重推出了 Design Support library,其中的杀手级控件CoordinatorLayout更是把 NestedScrolling 机制玩得出神入化。

变化

把 NestedScrolling 机制从 View 和 ViewGroup 中剥离,把有关的 API 放在接口中,把相关实现放在 Helper 里

优点

低版本的 View 也能嵌套滑动

缺点

使用麻烦,暴露了更多内部的不需要普通使用者关心的 API,影响开发者对整个机制的上手速度

第一个版本的Bug

惯性不连续 
在滑动内部 View 时快速抬起手指,内部 View 会开始惯性滑动,当内部 View 惯性滑动到自己顶部时便停止了滑动,此时外部的可滑动 View 不会有任何反应,即使外部 View 可以滑动。

第二个版本,2017年9月

2017年9月,Revision 26.1.0更新了一版NestedScrollingChild2和NestedScrollingParent2,并且处理了第一版中系统控件的Bug,区分开是用户手指移动触发的滑动还是由惯性触发的滑动

第二个版本的Bug

当外部 View 在顶部、内部 View 也在顶部时,往下滑动内部 View 然后快速抬起(制造 fling ),再马上滑外部 View 
预期应该是:外部 View 往上滚动 
但实际上:滑不动它,或是滑上去一点,马上又下来了

第三个版本,2018年11月

2018年11月5日androidx.core 1.1.0-alpha01的更新中,给出了最新的修复——NestedScrollingChild3和NestedScrollingParent3

第三个版本的Bug

当通过滑动内部 View 触发外部 View 滑动时,无法通过触摸外部 View 把它停下来

参考

https://blog.csdn.net/lmj623565791/article/details/52204039 
https://juejin.im/post/5c3c8d2ae51d4552475fcef7 
https://blog.csdn.net/zuguorui/article/details/78671480 
https://blog.csdn.net/tobacco5648/article/details/84667016 
https://blog.csdn.net/dqh147258/article/details/81208889 
https://blog.csdn.net/chen930724/article/details/50307193 
https://blog.csdn.net/lmj121212/article/details/52974427 
https://blog.csdn.net/happy_horse/article/details/54619526 
https://blog.csdn.net/fyfcauc/article/details/52415144

你可能感兴趣的:(Android,Android,嵌套滑动)