如果可滑动的视图嵌套了另外的可滑动的视图,就会产生滑动冲突。Android提供了一套父视图和子视图嵌套滑动交互的机制,主要包含的类如下:
- NestedScrollingChild/NestedScrollingChild2
- NestedScrollingChildHelper
- NestedScrollingParent/NestedScrollingParent2
- NestedScrollingParentHelper
源码解析
实现嵌套滚动的时候首先会定义一个视图实现NestedScrollingChild接口或者NestedScrollingChild2接口。
public interface NestedScrollingChild {
/**
* 启用或禁用此视图的嵌套滚动
* 如果设置为true,则将允许该视图启动嵌套滚动操作。如果此视图未实现嵌套滚动,则无效。在嵌套滚动进行过程中禁用嵌套滚动具有停止嵌套滚动的效果
* @param enabled 返回true允许嵌套滚动,false禁止嵌套滚动
*/
void setNestedScrollingEnabled(boolean enabled);
/**
* 如果为此视图启用了嵌套滚动,则返回true
* 如果启用了嵌套滚动,并且视图类支持嵌套滚动,则该视图将充当嵌套滚动子视图,并将正在滚动的数据转发给支持嵌套滚动的父视图
*/
boolean isNestedScrollingEnabled();
/**
* 沿指定轴开始嵌套滚动
* 开始嵌套滚动的视图将遵守以下约定:
* 该视图将在开始滚动操作(ACTION_DOWN事件)时调用startNestedScroll。在触摸滚动的情况下,嵌套滚动将以与requestDisallowInterceptTouchEvent(boolean)相同的方式自动终止。必须显式调用stopNestedScroll()来结束嵌套滚动的结尾。
* 如果startNestedScroll返回true,则说明有嵌套滚动的父视图。如果返回false,则调用者可以忽略其他方法,直到下一次滚动为止。当嵌套滚动已经进行时调用startNestedScroll将返回true。
* 在滚动中,调用者计算计算出滚动量后,都应调用dispatchNestedPreScroll。如果返回true,则嵌套滚动父级至少会部分消耗滚动,调用方应调整滚动量。
* 在使用了剩余的滚动量后,调用者应调用dispatchNestedScroll,同时传递消耗的和未消耗的滚动量。嵌套滚动父视图可能会不同地对待这些值。
* @param axes 值为SCROLL_AXIS_HORIZONTAL或SCROLL_AXIS_VERTICAL
*/
boolean startNestedScroll(int axes);
/**
* 停止嵌套滚动
* 如当前未进行嵌套滚动时调用此方法是无效的
*/
void stopNestedScroll();
/**
* 如果视图具有嵌套的滚动父视图,则返回true
* 有嵌套滚动父视图就说明视图已发起嵌套滚动,并且被视图层级结构中的祖先视图接受
*/
boolean hasNestedScrollingParent();
/**
* 对嵌套滚动的每一步进行调度分发
* 支持嵌套滚动的视图的实现应调用此方法来将滚动中的相关信息报告给嵌套滚动父视图。如果当前未进行嵌套滚动或对此视图未启用嵌套滚动,则此方法不执行任何操作。
* 兼容的View实现还应该在消费滚动事件本身之前调用dispatchNestedPreScroll
*
* @param dxConsumed 视图消耗的水平距离(像素)
* @param dyConsumed 视图消耗的垂直距离(像素)
* @param dxUnconsumed 视图未消耗的水平距离(像素)
* @param dyUnconsumed 视图未消耗的垂直距离(像素)
* @param offsetInWindow 可选值。如果不为null,则在返回时,此操作将包含此视图在此操作之前到完成之后在本地视图坐标中的偏移量
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
/**
* 为嵌套滚动中的父视图提供了在子视图使用它之前使用某些或全部滚动操作的机会。
*
* @param dx 水平滚动距离(像素)
* @param dy 垂直滚动距离(像素)
* @param consumed 输出。如果不为null,则consumed[0]将包含dx的消耗分量,而consumed[1]将包含消耗的垂直分量
* @param offsetInWindow 可选值。如果不为null,则在返回时,此操作将包含此视图在此操作之前到完成之后在本地视图坐标中的偏移量
*/
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
/**
* 将fling发送给嵌套滚动父视图
*
* @param velocityX 水平fling速度(像素/秒)
* @param velocityY 垂直fling速度(像素/秒)
* @param consumed 如果子视图消费掉了fling,则为true,否则为false
*/
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* 在视图处理前,将fling分配给嵌套滚动父视图
*
* @param velocityX 水平fling速度(像素/秒)
* @param velocityX 垂直fling速度(像素/秒)
*/
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
public interface NestedScrollingChild2 extends NestedScrollingChild {
boolean startNestedScroll(int axes, int type);
void stopNestedScroll(int type);
boolean hasNestedScrollingParent(int type);
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type);
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type);
}
NestedScrollingChild2继承了NestedScrollingChild类并且扩展了多了参数type的方法,方法中的type指的是导致此滚动事件的输入类型,值为TYPE_TOUCH=0或者TYPE_NON_TOUCH=1,如果type为TYPE_TOUCH表示输入类型是来自用户触摸屏幕的,如果type为TYPE_NON_TOUCH表示输入类型是由用户非触摸屏幕(通常为fling)引起的。接口所有方法的实现都交给NestedScrollingChildHelper来处理,所以就需要创建一个NestedScrollingChildHelper对象helper:
public class NestedScrollingChildHelper {
...
private final View mView;
...
public NestedScrollingChildHelper(@NonNull View view) {
this.mView = view;
}
...
}
可以看到在创建NestedScrollingChildHelper对象的时候,传入了子视图对象。然后会在创建完NestedScrollingChildHelper对象以后就会调用setNestedScrollingEnabled方法设置子视图是否支持嵌套滚动:
public class NestedScrollingChildHelper {
...
private final View mView;
...
public NestedScrollingChildHelper(@NonNull View view) {
this.mView = view;
}
public void setNestedScrollingEnabled(boolean enabled) {
if (this.mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(this.mView);
}
this.mIsNestedScrollingEnabled = enabled;
}
...
}
如果之前就允许嵌套滚动,则停止嵌套滚动。在子视图的onTouchEvent方法中,会判断事件是否是ACTION_DOWN,如果是则会调用startNestedScroll开始进行嵌套滚动:
public class NestedScrollingChildHelper {
...
public boolean startNestedScroll(int axes, int type) {
if (this.hasNestedScrollingParent(type)) {
return true;
} else {
if (this.isNestedScrollingEnabled()) {
ViewParent p = this.mView.getParent();
for(View child = this.mView; p != null; p = p.getParent()) {
if (ViewParentCompat.onStartNestedScroll(p, child, this.mView, axes, type)) {
this.setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, this.mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View)p;
}
}
}
return false;
}
}
}
先看看hasNestedScrollingParent方法:
public class NestedScrollingChildHelper {
...
public boolean hasNestedScrollingParent(int type) {
return this.getNestedScrollingParentForType(type) != null;
}
private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
switch (type) {
case TYPE_TOUCH:
return mNestedScrollingParentTouch;
case TYPE_NON_TOUCH:
return mNestedScrollingParentNonTouch;
}
return null;
}
...
}
在没有设置之前返回null,所以startNestedScroll进入else分支中。先看看ViewParentCompat.onStartNestedScroll是什么:
public final class ViewParentCompat {
...
public static boolean onStartNestedScroll(ViewParent parent, View child, View target, int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
return ((NestedScrollingParent2)parent).onStartNestedScroll(child, target, nestedScrollAxes, type);
} else {
if (type == ViewCompat.TYPE_TOUCH) {
if (VERSION.SDK_INT >= 21) {
try {
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError var6) {
Log.e("ViewParentCompat", "ViewParent " + parent + " does not implement interface " + "method onStartNestedScroll", var6);
}
} else if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent)parent).onStartNestedScroll(child, target, nestedScrollAxes);
}
}
return false;
}
}
...
}
从代码看出来就是判断parent是否实现了NestedScrollingParent2接口或者NestedScrollingParent接口,如果实现了直接调用onStartNestedScroll的方法并返回其值。如果没有实现,则判断父视图是否存在onStartNestedScroll,如果存在,直接调用并返回,如果都不满足直接返回false。
总的来说就是遍历视图的所有祖先视图,如果有视图接受嵌套滚动,则调用setNestedScrollingParentForType保存接受嵌套滚动的父视图,并且调用ViewParentCompat.onNestedScrollAccepted方法:
public final class ViewParentCompat {
...
public static void onNestedScrollAccepted(ViewParent parent, View child, View target, int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
((NestedScrollingParent2)parent).onNestedScrollAccepted(child, target, nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
if (VERSION.SDK_INT >= 21) {
try {
parent.onNestedScrollAccepted(child, target, nestedScrollAxes);
} catch (AbstractMethodError var6) {
Log.e("ViewParentCompat", "ViewParent " + parent + " does not implement interface " + "method onNestedScrollAccepted", var6);
}
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent)parent).onNestedScrollAccepted(child, target, nestedScrollAxes);
}
}
}
...
}
同样就是判断父视图是否实现了NestedScrollingParent2或者NestedScrollingParent接口,如果实现了,则调用接口的onNestedScrollAccepted方法,否则判断父视图是否存在onNestedScrollAccepted方法,如果存在则调用。
接下来看看ACTION_MOVE做的事情,在接收到ACTION_MOVE事件时,要调用dispatchNestedPreScroll方法和dispatchNestedScroll方法,dispatchNestedPreScroll方法用于在滚动的时候,通知嵌套滚动父视图滚动的距离,而dispatchNestedScroll方法用于在处理滚动之后通知嵌套滚动父视图滚动的距离:
public class NestedScrollingChildHelper {
...
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
if (this.isNestedScrollingEnabled()) {
ViewParent parent = this.getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
//如果不为空,则将赋值为视图滚动开始时的位置坐标
this.mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
if (this.mTempNestedScrollConsumed == null) {
this.mTempNestedScrollConsumed = new int[2];
}
consumed = this.mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, this.mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
//如果不为空
this.mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX; //计算x方向的偏移量并赋值
offsetInWindow[1] -= startY; //计算y方向的偏移量并赋值
}
//返回true则表示父视图有消费滚动量,否则没有
return consumed[0] != 0 || consumed[1] != 0;
}
if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
...
}
上面代码最关键的就是ViewParentCompat.onNestedPreScroll方法:
public final class ViewParentCompat {
...
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
((NestedScrollingParent2)parent).onNestedPreScroll(target, dx, dy, consumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
if (VERSION.SDK_INT >= 21) {
try {
parent.onNestedPreScroll(target, dx, dy, consumed);
} catch (AbstractMethodError var7) {
Log.e("ViewParentCompat", "ViewParent " + parent + " does not implement interface " + "method onNestedPreScroll", var7);
}
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent)parent).onNestedPreScroll(target, dx, dy, consumed);
}
}
}
...
}
同样是判断父视图是否实现NestedScrollingParent2或NestedScrollingParent接口,如果实现了,就调用onNestedPreScroll方法,否则判断父视图是否有方法onNestedPreScroll,如果有就调用。其实就是在视图滚动之前先让父视图处理。接下来再看看dispatchNestedScroll方法:
public class NestedScrollingChildHelper {
...
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
if (this.isNestedScrollingEnabled()) {
ViewParent parent = this.getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
this.mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
ViewParentCompat.onNestedScroll(parent, this.mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
if (offsetInWindow != null) {
this.mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
}
if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
...
}
主要在方法ViewParentCompat.onNestedScroll的调用:
public final class ViewParentCompat {
...
public static void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (parent instanceof NestedScrollingParent2) {
((NestedScrollingParent2)parent).onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
if (VERSION.SDK_INT >= 21) {
try {
parent.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
} catch (AbstractMethodError var8) {
Log.e("ViewParentCompat", "ViewParent " + parent + " does not implement interface " + "method onNestedScroll", var8);
}
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent)parent).onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}
}
}
...
}
判断父视图是否实现NestedScrollingParent2或NestedScrollingParent接口,如果实现了,就调用onNestedScroll方法,否则判断父视图是否有方法onNestedScroll,如果有就调用。其实就是在视图滚动之前先让父视图处理。
最后就是在ACTION_UP事件中调用stopNestedScroll方法:
public class NestedScrollingChildHelper {
...
public void stopNestedScroll(int type) {
ViewParent parent = this.getNestedScrollingParentForType(type);
if (parent != null) {
ViewParentCompat.onStopNestedScroll(parent, this.mView, type);
this.setNestedScrollingParentForType(type, (ViewParent)null);
}
}
...
}
再看看ViewParentCompat.onStopNestedScroll方法:
public final class ViewParentCompat {
...
public static void onStopNestedScroll(ViewParent parent, View target, int type) {
if (parent instanceof NestedScrollingParent2) {
((NestedScrollingParent2)parent).onStopNestedScroll(target, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
if (VERSION.SDK_INT >= 21) {
try {
parent.onStopNestedScroll(target);
} catch (AbstractMethodError var4) {
Log.e("ViewParentCompat", "ViewParent " + parent + " does not implement interface " + "method onStopNestedScroll", var4);
}
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent)parent).onStopNestedScroll(target);
}
}
}
...
}
接下来看看需要嵌套滚动父视图实现的NestedScrollingParent和NestedScrollingParent2接口:
public interface NestedScrollingParent {
boolean onStartNestedScroll(View child, View target, int axes);
void onNestedScrollAccepted(View child, View target, int axes, int type);
void onStopNestedScroll(View target);
void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
boolean onNestedPreFling(View target, float velocityX, float velocityY);
int getNestedScrollAxes();
}
public interface NestedScrollingParent2 extends NestedScrollingParent {
boolean onStartNestedScroll(View child, View target, int axes, int type);
void onNestedScrollAccepted(View child, View target, int axes, int type);
void onStopNestedScroll(View target, int type);
void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type);
void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type);
}
上面分析子视图的调用的时候其实也看到了父视图对称的调用。再看看NestedScrollingParentHelper类:
public class NestedScrollingParentHelper {
private final ViewGroup mViewGroup;
private int mNestedScrollAxes;
public NestedScrollingParentHelper(@NonNull ViewGroup viewGroup) {
mViewGroup = viewGroup;
}
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
@ScrollAxis int axes) {
onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);
}
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type) {
mNestedScrollAxes = axes;
}
@ScrollAxis
public int getNestedScrollAxes() {
return mNestedScrollAxes;
}
public void onStopNestedScroll(@NonNull View target) {
onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
}
public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
mNestedScrollAxes = 0;
}
}
NestedScrollingParentHelper就是存了mNestedScrollAxes值而已,没什么逻辑。
至于未提到的方法dispatchNestedPreFling和dispatchNestedFling逻辑也是相同的,到这里源码差不多过完了,接下来梳理一下类和方法的关系以及调用流程。
首先 NestedScrollingChild/NestedScrollingChild2接口是给子视图定义规则的,而NestedScrollingParent/NestedScrollingParent2接口是给嵌套滚动父视图定义规则的。
其此NestedScrollingChild/NestedScrollingChild2接口都委托给NestedScrollingChildHelper的方法了。NestedScrollingChildHelper的每个方法中都会先做一些验证或数据准备工作,然后通过ViewParentCompat类对应的方法将调用传达给嵌套父视图,也就是ViewParentCompat类是连接子视图和嵌套父视图的桥梁。ViewParentCompat类调用父视图是通过接口NestedScrollingParent/NestedScrollingParent2或者父视图本身存在的方法进行调用的。
感谢大家的支持,如有错误请指正,如需转载请标明原文出处!