三探Android嵌套滑动 NestedScrolling机制 本质以及源码解析

时间:2022-09-08 12:03:53

要了解NestedScrolling机制的本质,当然少不了阅读源码。

这里我们先给出结论:NestedScrolling机制本质上就是两个相互关联的接口,当我们调用一个接口中的方法时,另一个接口中与之对应的方法就会被触发,仅此而已。

这就意味着,尽管我们之前介绍NestedScrolling机制时,为其加了很多条条框框和使用规则,但实际上,我们可以按照自己的需求和想法,完全*的去使用它们————只要知道两个接口中方法的对应关系即可,至于何时调用NestedScrollingChild接口中的方法以及在NestedScrollingParent的方法中要做什么,都随你意。以NestedScrollingParent的onNestedPreScroll()方法为例:你可以使用scrollTo()、scrollBy()来滚动自身的内容(此时自身的布局位置是不变的),也可以通过修改自己的layoutParams来改变自身的布局位置;你甚至可以明明滚动了50px却向回传参数谎报说自己1px都没有滚动……只要能实现你想要的效果就行。

1源码解析

1.1 NestedScrollingChild和NestedScrollingParent

public interface NestedScrollingChild { 
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);

public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean hasNestedScrollingParent();
}

public interface NestedScrollingParent {
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

这两个接口其实没什么好说的,仅仅就是定义了一些抽象方法而已。抽象方法能做什么事取决于它的具体实现。而在第一篇文章中我们已经知道,对于实现这两个接口中的大部分方法,我们只要调用其对应的helper类中的同名方法即可。

下面的表格我们在第一篇文章中也已经见到过了,它描述的是两个接口中方法的触发关系:

NestedScrollingChild中的方法(发起者) NestedScrollingParent中的方法(被回调)
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll

这些方法之间的触发关系是如果建立起来的呢?——通过NestedScrollingChildHelper对象。下面我们将通过阅读NestedScrollingChildHelper的源码来明确这一点。

1.2 NestedScrollingChildHelper

NestedScrollingChildHelper完整源码(中文注释)

在看源码之前先说明一点:为了方便表述以及避免混淆,以下我们都将使用配合者parent发起者child特指通过NestedScrolling机制进行配合动作的一对父子view。

先来看成员变量

private final View mView;//发起者child
private ViewParent mNestedScrollingParent;//配合者parent
private boolean mIsNestedScrollingEnabled;

public NestedScrollingChildHelper(View view) {
mView = view;
}

public boolean hasNestedScrollingParent() {
return mNestedScrollingParent != null;
}

public void setNestedScrollingEnabled(boolean enabled) {
...
mIsNestedScrollingEnabled = enabled;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • mView:发起者child,在创建NestedScrollingChildHelper对象时,由构造方法传入
  • mNestedScrollingParent:在startNestedScroll(int axes)方法中找到的配合者parent
  • mIsNestedScrollingEnabled:相当于是一个功能开关,如果值为false的话,那么NestedScrolling机制就无法使用

startNestedScroll(int axes)方法

这个方法所做的事情就是自下而上遍历mView的各级父view,看其中是否存在一个实现了NestedScrollingParent接口并且其onStartNestedScroll(…)方法返回true的父view,如果存在,则将这个父view赋值给成员变量mNestedScrollingParent,并返回true(表示找到了能与发起者child进行配合动作的配合者parent)。

具体做法参考代码及注释:

public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
return true;
}

if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;

//往上逐层调用每个父view的onStartNestedScroll方法,直到某个父view的onStartNestedScroll返回了ture,
//此时说明找到了配合者parent
while (p != null) {
//这几个参数的含义参考NestedScrollParent接口的onStartNestedScroll
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {//找到了配合者parent
mNestedScrollingParent = p;//保存配合者parent
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);//调用配合者parent的onNestedScrollAccepted方法
return true;
}

if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}

return false;
}
  • 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
  • 28
  • 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
  • 28

stopNestedScroll()方法

就是简单的调用配合者parent的onStopNestedScroll方法而已

public void stopNestedScroll() {
if (mNestedScrollingParent != null) {
//调用配合者parent的onStopNestedScroll方法
ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
mNestedScrollingParent = null;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

dispatchNestedPreScroll(…)方法

就做了两件事:

  • 1.调用配合者parent的onNestedPreScroll方法
  • 2.根据配合者parent的起止位置计算offsetInWindow
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {

//获得配合者parent的起始位置
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}

//将comsumed清空
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;

//调用配合者parent的onNestedPreScroll方法
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

//根据配合者parent的起始位置和终止位置计算窗体偏移量(其实就是配合者parent的偏移量)
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;
}
  • 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
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 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
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

dispatchNestedScroll(…)方法

这个与上面的dispatchNestedPreScroll()方法如出一辙,不用解释了吧

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {

//获得配合者parent的起始位置
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}

//调用配合者parent的onNestedScroll方法
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);

//根据配合者parent的起始位置和终止位置计算窗体偏移量(其实就是配合者parent的偏移量)
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
  • 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
  • 28
  • 29
  • 30
  • 31
  • 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
  • 28
  • 29
  • 30
  • 31

dispatchNestedPreFling(…)和dispatchNestedFling(…)方法

这两个方法可以类比于dispatchNestedPreScroll(…)和dispatchNestedScroll(…),但是更简单,只做了“调用配合者parent的同名方法”这一件事。

public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
//调用配合者parent的onNestedPreFling方法
return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX, velocityY);
}
return false;
}

public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
//调用配合者parent的onNestedFling方法
return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX, velocityY, consumed);
}
return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

以上就是NestedScrollingChildHelper的主要代码。

总结一下,NestedScrollingChildHelper主要就是做了下面这两件事

  1. 找到配合者parent
  2. 作为发起者child和配合者parent之间方法调用的桥梁,起一个中介或者说是代理的作用。

    当我们调用NestedScrollingChild中的方法XXX()时,方法XXX()实际会去调用NestedScrollingChildHelper中的方法XXX(),而NestedScrollingChildHelper中的方法XXX()又会去调用NestedScrollingParent中的方法onXXX(),就是这样一个简单的传递流程。方法的返回值则是走相反的传递路径。

1.3 NestedScrollingParentHelper

源码只是寥寥数行,没有什么值得特别注意的地方:

//此Helper类的工作非常简单,就是保存了axes的信息而已
public class NestedScrollingParentHelper {
private final ViewGroup mViewGroup;
private int mNestedScrollAxes;

public NestedScrollingParentHelper(ViewGroup viewGroup) {
mViewGroup = viewGroup;
}

public void onNestedScrollAccepted(View child, View target, int axes) {
mNestedScrollAxes = axes;
}

public int getNestedScrollAxes() {
return mNestedScrollAxes;
}

public void onStopNestedScroll(View target) {
mNestedScrollAxes = 0;
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

2总结与思考

回想NestedScrolling的工作流程并结合上面helper类的源码,我们会发现整个NestedScrolling机制其实就是两个接口加上一个中介(NestedScrollingChildHelper)而已。在两个helper类中也基本没有涉及到接口的使用方式————这也就是为什么我们会在文章开头时说:你可以在一定范围内“为所欲为”。

google对于NestedScrolling机制的设计也很值得我们在自己的项目中借鉴:

  1. 通过两个接口来解耦需要进行交互的view
  2. 提供封装了接口之间交互逻辑的helper类以方便用户使用接口

下一篇文章中,我们将使用NestedScrolling机制实现系列文章开始时所示的饿了么店铺详情页效果。