要了解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主要就是做了下面这两件事:
- 找到配合者parent
-
作为发起者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机制的设计也很值得我们在自己的项目中借鉴:
- 通过两个接口来解耦需要进行交互的view
- 提供封装了接口之间交互逻辑的helper类以方便用户使用接口
下一篇文章中,我们将使用NestedScrolling机制实现系列文章开始时所示的饿了么店铺详情页效果。