前言
CoordinatorLayout是Google为了简化Android中复杂的嵌套交互逻辑的实现而设计的一种ViewGroup;它的出现在当时可谓是惊艳了Android的世界,因为在此之前,如果想要实现一种复杂的嵌套交互逻辑,是一件非常烧脑的事情,而CoordinatorLayout刚好解决了这个难题,它之所以这么强大,离不开它内部的Behavior类的帮忙,接下来就让我们走进源码,一起了解下Behavior的实现原理。
Behavior
1.定义:
用于实现CoordinatorLayout子视图交互行为的插件;我们先来看下它的类的定义以及构造函数的源码:
public static abstract class Behavior<V extends View> {
/** Default constructor for instantiating Behaviors.*/
public Behavior() {
}
/**
* Default constructor for inflating Behaviors from layout. The Behavior will have
* the opportunity to parse specially defined layout parameters. These parameters will
* appear on the child view tag.
*/
public Behavior(Context context, AttributeSet attrs) {
}
......
}
Behavior作为CoordinatorLayout的内部类,它有两个默认的构造方法,第一个无参构造可用于在代码中动态的创建一个Behavior,另一个含有Context和AttributeSet类型的两个参数的构造方法,用于在xml布局中指定Behavior时使用,注意这里一定要记住,如果我们是在xml中给某个View指定我们自定义的Behavior时,那么我们自定义的Behavior一定要实现这个两个参数的构造方法,否则View是不会关联上这个Behavior的,原因还是要看下CoordinatorLayout的LayoutParams的源码:
LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CoordinatorLayout_Layout);
......
mBehaviorResolved = a.hasValue( // 注释1
R.styleable.CoordinatorLayout_Layout_layout_behavior);
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString( // 注释2
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
a.recycle();
if (mBehavior != null) { // 注释3
// If we have a Behavior, dispatch that it has been attached
mBehavior.onAttachedToLayoutParams(this);
}
}
上述代码是CoordinatorLayout的LayoutParams类的构造方法的源码,在注释1处可以看到,LayoutParams内部会先判断我们是否在xml中给View设置了layout_behavior属性,如果layout_behavior属性有值的话,那么再调用注释2处的parseBehavior方法来解析Behavior实例,关于parseBehavior方法的源码这里我就不贴出来的,在它的内部会根据layout_behavior属性指定的string字符串(就是Behavior类的完整包路径和类名)通过ClassLoader来加载出对应的Behavior类,然后获取Behavior类的两个参数的构造方法来初始化Behavior实例;如果我们没有实现两个参数的构造方法,那么在这里就会抛出运行时异常,如下:
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
因此这里使用时记得要注意一下,接下来我们就看下Behavior中一些重要方法的源码!
2.使用:
在开始阅读Behavior的源码之前,我们先来明确一下CoordinatorLayout的使用场景,文章的开头提到过CoordinatorLayout是为了实现复杂的嵌套交互逻辑而设计的。所谓嵌套,就肯定不是一个View的事情了,应该是两个或两个以上的View之间存在着交互依赖的关系,那么我们就要知道Behavior是如何帮助不同的View之间确立依赖关系的。
1.和依赖相关的API
(1) layoutDependsOn
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull View dependency) {
return false;
}
该方法用于给使用当前Behavior的View确定依赖的视图,当使用当前Behavior的View有layout请求时,该方法至少会被调用一次;方法中的第一个参数就是当前的父容器CoordinatorLayout,第二个参数child就是当前Behavior绑定的View,第三个参数dependency就是和当前View同级的兄弟视图,如果当前的View依赖于dependency,那么就可以设置layoutDependsOn方法的返回值为true,这样当dependency视图的的布局或位置发生改变时,当前Behavior的onDependentViewChanged方法就会被调用;而如何确定dependency视图就是当前的View所依赖的视图呢?我个人这里有两种判断方式的建议,一种我称之为精确判断,就是可以根据View的某一具体属性作为判断依据,比如通过比较View的id:
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull View dependency) {
return dependency.getId() == R.id.dependency_view; // 假设给布局中的某个View指定了该id
}
另一种方法我称之为模糊判断,比如当前的View依赖于某一种类型的View,如下:
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull View dependency) {
return dependency instanceof DependencyView; // 假设存在自定义控件DependencyView
}
根据自己的实际需求来决定判断的依据,这里我只是提供两个思路,接下来我们再来看刚刚提到的onDependentViewChanged方法;
(2) onDependentViewChanged
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull View dependency) {
return false;
}
前面也已经说过这个方法的调用时机了,当layoutDependsOn方法返回为true并且依赖的视图的位置或大小发生改变时,该方法才会被调用,方法的前两个参数意义和layoutDependsOn方法中的一样,而第三个参数这时就已经确定是当前View所依赖的某个同级的兄弟视图了;而该方法的返回值的意义是,如果通过Behavior改变了当前View的大小或位置时,方法返回true,否则返回false,举个简单的例子如下:
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull View dependency) {
int dependLeft = dependency.getLeft(); // 获取依赖视图的左侧位置
int dependTop = dependency.getTop(); // 获取依赖视图的顶部位置
child.setX(dependLeft);
child.setY(dependTop);
return true;
}
上述例子中当前的View的位置会紧随依赖视图的位置变化而变化,因此返回true,而如果在该方法中只是单纯做一些事件调用记录操作而不会改变当前View的大小和位置的话,就返回false即可,例如:
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child,
@NonNull View dependency) {
Toast.makeText(parent.getContext(), "我依赖的View位置或大小发生了改变", Toast.LENGTH_SHORT).show();
return false;
}
(3) 小结
Behavior中关于View之间依赖关系设置和操作的两个重要的api方法就分析完了,其实还有一个onDependentViewRemoved方法,该方法在当前View所依赖的视图从父容器CoordinatorLayout中移除时被调用,相比于前两个方法它的使用频次相对较少,更多的是用来记录一下当前CoordinatorLayout容器中子控件的一些状态,感兴趣的同学可以自己阅读下源码,这里就不赘述了。
2.和嵌套滑动相关的API
首先要明确什么是嵌套滑动,因为有嵌套两个字,所以肯定不是一个View的滑动了,而是两个或两个以上的View进行滑动交互的效果。而这里面肯定会有一个嵌套滑动的发起者,针对这个发起者,我们先提出这样一个疑问,是不是任意一个类型的View都可以发起嵌套滑动,带着这样的疑问我们来看下和嵌套滑动相关的第一个重要的api方法:
(1) onStartNestedScroll
/**
* @param coordinatorLayout 当前的CoordinatorLayout
* @param child 当前Behavior关联的CoordinatorLayout的子视图
* @param directTargetChild CoordinatorLayout的子视图,有可能是嵌套滑动操作的目标,也有可能包含嵌套滑动操作的目标
* @param target 发起嵌套滑动的CoordinatorLayout的子视图
* @param axes 此嵌套滚动所应用的轴,一种X,一种Y
* @return 如果当前Behavior希望接受此嵌套滚动,则返回true
*/
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes) {
return false;
}
当CoordinatorLayout中的任何一个子View尝试触发嵌套滑动的时候此方法都会被调用,该方法的返回值代表当前Behavior关联的View是否接受此嵌套滑动,如果当前方法返回true,那么后续的几个和嵌套滑动相关的方法才会相继被调用,否则代表当前Behavior关联的View不响应此嵌套滑动;上述源码中关于几个参数的含义都有阐述,其中最后一个int类型的参数代表触发嵌套滑动的方向轴,有两种取值,为View源码中的两个静态常量,分别为SCROLL_AXIS_HORIZONTAL和SCROLL_AXIS_VERTICAL,分别代表水平方向和竖直方向;在处理当前方法的返回值时,我们可以根据需求场景来选择接受哪个方向轴的嵌套滑动,例如:
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes) {
return target instanceof RecyclerView && axes == View.SCROLL_AXIS_VERTICAL;
}
上述示例代表如果发起嵌套滑动的View是RecyclerView并且滑动方向为Y轴方向,就接受此嵌套滑动。(PS:接下来说到的几个和嵌套滑动相关的方法被调用的前提都是当前这个onStartNestedScroll方法返回为true!!!)
(2) onNestedPreScroll
/**
* @param dx 用户试图滚动的原始水平像素数
* @param dy 用户试图滚动的原始垂直像素数
* @param consumed consumed[0]应该等于dx上被消耗的距离,consumed[1]应该等于dy上被消耗的距离
*/
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed) {
}
源码中针对此方法的解释是当一个嵌套滑动即将更新,在目标消耗滚动距离之前被调用;也就是当前Behavior关联的View即将进行嵌套滑动时被调用,方法中的前三个参数都和onStartNestedScroll方法中对应的那三个参数的含义一样,dx参数的意义为当前嵌套滑动试图滚动的原始水平像素数,dy参数的意义就是当前嵌套滑动试图滚动的原始垂直像素数,而最后一个数组consumed参数,我们在执行嵌套滑动过程中,应该将当前Behavior关联的View在dx上所消耗的距离设置到consumed[0]上,而在dy上所消耗的距离设置到consumed[1]上,其目的是让父容器CoordinatorLayout消耗掉滑动距离,确保发起嵌套滑动操作的控件的内部不发生滚动;这里我这么说可能比较抽象,我举个简单的例子,假如在一个页面上,顶部有一个Title文本控件,在它的下面是一个RecyclerView,当Title文本还在屏幕上方显示时,我们希望手指触摸RecyclerView上滑,此时Title文本和RecyclerView整体上移,而RecyclerView的条目item不发生滚动复用,当Title文本移出屏幕后RecyclerView的条目item才会发生滚动复用,那么要想实现这个效果,在Title控件的Behavior的onNestedPreScroll方法中,就要将Title控件在嵌套滑动过程中消耗掉dx和dy的值设置到这个consumed数组上,如果不设置那么就会出现在Title文本和RecyclerView整体上移的过程中,RecyclerView的内容条目也发生滚动,这种效果显然不太好。关于这个consumed参数的意义我就说这些,感兴趣的同学可以自己动手去验证一下实际效果。
(3) onNestedScroll
/**
* @param dxConsumed target自身滚动操作消耗的水平像素
* @param dyConsumed target自身滚动操作消耗的垂直像素
* @param dxUnconsumed 由用户发起的滑动操作,但是没有被target自身滚动操作所消耗掉的水平像素
* @param dyUnconsumed 由用户发起的滑动操作,但是没有被target自身滚动操作所消耗掉的垂直像素
*/
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
源码中针对此方法的解释为当一个嵌套滑动正在进行并且目标视图已经滚动或者尝试去滚动时调用,该方法和刚刚说的onNestedPreScroll方法其实没有什么太大的区别,只不过在调用时机上onNestedPreScroll方法会先它一步,如果我们在onNestedPreScroll方法中没有做任何的操作,那么在该方法中的dxConsumed和dyConsumed的参数值会分别和onNestedPreScroll方法中的dx和dy参数值一致,而dxUnconsumed和dyUnconsumed值均为0,其实看上面源码中关于这四个参数的解释我们就能明白为什么会这样,因为在onNestedPreScroll方法中没有任何的操作,也就是说当前Behavior关联的View没有消耗任何的嵌套滑动,那么滑动事件就肯定被发起嵌套滑动事件的View自身所全部消耗了。另外,其实这两个方法中我们都可以实现对View的嵌套滑动逻辑,只是调用时机有先后,如果在嵌套滑动过程中我们不需要对发起嵌套滑动的View有什么滚动逻辑的限制的话,那么在这两个方法的选择上没有什么太大的区别。
(4) onNestedPreFling
/**
* @param velocityX 抛掷的水平速度
* @param velocityY 抛掷的垂直速度
*/
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target, float velocityX, float velocityY) {
return false;
}
当发起嵌套滑动的子View准备发起抛掷操作时,该方法被调用,方法中的后两个参数分别代表抛掷操作在水平和竖直方向的抛掷速度,如果该方法返回true,代表当前的Behavior消耗了抛掷操作。这个方法通常用来实现一些特殊的边缘效果,比如当手指快速滑动RecyclerView,然后手指离开RecyclerView的时候列表还没有停止滚动,这时就是一个抛掷动作,onNestedPreFling方法就会被调用,我们可以在方法中进行一个Toast提示,提示用户“干嘛滑得这么快”,当然我是举例,肯定没有几个产品会有这么牛逼的效果的。另外,和抛掷操作有关的还有一个onNestedFling方法,这个方法和onNestedPreFling方法的关系就像上面说的两个方法一样,没有什么太大的区别,只是调用时机不同,一个是在抛掷操作准备执行时被调用,一个是在抛掷操作正在进行时被调用,这里就不赘述了。
(5) onStopNestedScroll
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child, @NonNull View target) {
}
当嵌套滑动结束时被调用,方法中的几个参数不必过多的解释了,该方法主要用来在嵌套滑动结束时做一些收尾工作,比如对嵌套滑动的状态进行更新等,具体怎么使用还要根据实际业务需求来定。
(6) 小结
Behavior中和嵌套滑动相关联的几个重要的api方法就看完了,现在对Behavior是如何帮助CoordinatorLayout完成嵌套滑动的逻辑有了更深的了解了吧。现在我们再来回顾一下之前留下的一个疑问,是不是任意一个类型的View都可以发起嵌套滑动!
NestedScrollingChild
1.定义:
这是一个接口,它应该被一个想要发起嵌套滑动事件到父容器中的View所实现。这是源码中关于这个接口的解释,在这个解释中我们看到了嵌套滑动这四个熟悉的字眼,说明NestedScrollingChild这个接口和嵌套滑动肯定有着一定的联系的。
2.回归源码:
在讲Behavior和嵌套滑动相关的api方法时,我们第一个说到的方法是onStartNestedScroll方法,这个方法是Behavior响应嵌套滑动事件的开始,如果这个方法没有返回true,那么后续的刚刚说过的几个和嵌套滑动相关的api方法就都不会被调用了。那么到底是为什么呢,我们先来看下这个方法被调用的地方:
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type); // 注释1
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted); // 注释2
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
这是CoordinatorLayout的onStartNestedScroll方法的源码,注意看代码中的注释1那一行,Behavior中的onStartNestedScroll方法就是在这里调用的,方法的返回值会赋值到变量accepted上,然后在注释2处调用LayoutParams的setNestedScrollAccepted方法将刚刚被赋值的accepted变量作为参数传入进方法中,注意这里的LayoutParams是CoordinatorLayout的内部类,我们先来看下setNestedScrollAccepted方法的源码:
void setNestedScrollAccepted(int type, boolean accept) {
switch (type) {
case ViewCompat.TYPE_TOUCH:
mDidAcceptNestedScrollTouch = accept;
break;
case ViewCompat.TYPE_NON_TOUCH:
mDidAcceptNestedScrollNonTouch = accept;
break;
}
}
方法中会首先对type变量进行判断,如果是ViewCompat.TYPE_TOUCH类型,即当前手势为用户手指触摸屏幕时,将accept参数的值赋值给LayoutParams内部的成员变量mDidAcceptNestedScrollTouch,方法中的这个type常规情况都是ViewCompat.TYPE_TOUCH类型,所以这里我们忽略非此类型的情况;接下来我们再看下这个mDidAcceptNestedScrollTouch变量在LayoutParams内部的使用情况,发现除了刚刚赋值的地方外,就只有一处被调用的地方在LayoutParams的isNestedScrollAccepted方法中,如下:
boolean isNestedScrollAccepted(int type) {
switch (type) {
case ViewCompat.TYPE_TOUCH:
return mDidAcceptNestedScrollTouch;
case ViewCompat.TYPE_NON_TOUCH:
return mDidAcceptNestedScrollNonTouch;
}
return false;
}
这里我们还是只考虑type为ViewCompat.TYPE_TOUCH的情况,mDidAcceptNestedScrollTouch变量会作为isNestedScrollAccepted方法的返回值被返回,而这个isNestedScrollAccepted方法的返回值便是前面说到的几个和嵌套滑动相关的api方法是否会被调用的关键。为了证明这一点我们来看下CoordinatorLayout的onNestedPreScroll方法的源码:
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
.......
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) { // 注释1
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type); // 注释2
........
}
}
........
}
这里我只保留了一些关键代码,我们可以看到在注释1处会对LayoutParams的isNestedScrollAccepted方法的返回值进行判断,如果返回true,才会继续向下执行在注释2处调用Behavior的onNestedPreScroll方法,否则是不会执行Behavior的onNestedPreScroll方法的。Behavior另外几个和嵌套滑动相关的方法在被调用之前也会经过这个判断,感兴趣的同学可以自行阅读源码,这里就不一一验证了。
现在我们证实了Behavior的onStartNestedScroll方法的返回值确实会影响后续几个和嵌套滑动相关的方法的调用,接下来继续回到CoordinatorLayout的onStartNestedScroll方法,在这个方法中我们看到了Behavior的onStartNestedScroll方法的调用,但是CoordinatorLayout的onStartNestedScroll方法是怎样被调用的呢。我们继续查看源码,在View和ViewParentCompat类中我们看到了该方法的调用,因为我们想知道是不是任意类型的View都可以发起嵌套滑动,因此我们点击进入View的源码,先看源码:
public boolean startNestedScroll(int axes) {
........
if (isNestedScrollingEnabled()) { // 注释1
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
if (p.onStartNestedScroll(child, this, axes)) { // 注释2
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
} catch (AbstractMethodError e) {
........
}
........
return false;
}
同样也是保留了关键代码,在注释1处会调用View的isNestedScrollingEnabled方法进行判断,方法返回true时才会进入if语句并在注释2处继续调用到CoordinatorLayout的onStartNestedScroll方法,现在可以知道了只有当View的isNestedScrollingEnabled方法返回true时,这个View才可以发起嵌套滑动操作,那么我们就来看下isNestedScrollingEnabled方法的源码:
/**
* @see #setNestedScrollingEnabled(boolean)
*/
public boolean isNestedScrollingEnabled() {
return (mPrivateFlags3 & PFLAG3_NESTED_SCROLLING_ENABLED) ==
PFLAG3_NESTED_SCROLLING_ENABLED;
}
即当mPrivateFlags3变量的值与PFLAG3_NESTED_SCROLLING_ENABLED常量值进行与运算后是否还是PFLAG3_NESTED_SCROLLING_ENABLED常量的值,要想满足这一条件那么mPrivateFlags3的取值可能有好几种,如果这样找的话会很浪费时间,还好源码在方法的注释中提到了setNestedScrollingEnabled方法,我们就来看下setNestedScrollingEnabled方法的源码:
/**
* Enable or disable nested scrolling for this view
* @see #isNestedScrollingEnabled()
*/
public void setNestedScrollingEnabled(boolean enabled) {
if (enabled) {
mPrivateFlags3 |= PFLAG3_NESTED_SCROLLING_ENABLED;
} else {
stopNestedScroll();
mPrivateFlags3 &= ~PFLAG3_NESTED_SCROLLING_ENABLED;
}
}
方法的注释直接就说到该方法是用来设置View是否可以进行嵌套滑动的,现在我们离答案更近一步了,我们可以提出这样一个猜测,是不是只要我们在代码中调用View的setNestedScrollingEnabled方法并且传入true,那么这个View便可以发起嵌套滑动了,是的没错,但是要求是在系统API在21以上时,那21以下时我们该怎么办呢?我们来看下ViewCompat类中的setNestedScrollingEnabled方法的源码吧:
public static void setNestedScrollingEnabled(@NonNull View view, boolean enabled) {
if (Build.VERSION.SDK_INT >= 21) {
view.setNestedScrollingEnabled(enabled);
} else {
if (view instanceof NestedScrollingChild) {
((NestedScrollingChild) view).setNestedScrollingEnabled(enabled);
}
}
}
在API大于等于21时,直接调用View的setNestedScrollingEnabled方法即可,当API小于21时,需要判断View是否是实现了NestedScrollingChild接口的子类,如果是才会继续调用NestedScrollingChild接口的setNestedScrollingEnabled方法设置View是否支持嵌套滑动。因此这里得出了两个结论,第一,当API小于21时要想让一个View支持嵌套滑动的前提是这个View必须实现了NestedScrollingChild接口;第二,在满足结论一的前提下,如果想让一个View支持嵌套滑动可以通过调用ViewCompat的setNestedScrollingEnabled方法传入true。
3.小结:
现在应该明白为什么要单独拎出NestedScrollingChild来分析了吧,因为目前国内大部分应用不可能做到完全不适配5.0以下的系统,所以如果应用中涉及到想让一个“普通”的View支持发起嵌套滑动逻辑的话那么View就一定要实现这个NestedScrollingChild接口。另外,刚刚说的普通的View是指目前系统提供的大部分View都没有默认实现这个NestedScrollingChild接口,而目前我知道的默认实现了NestedScrollingChild接口的View有NestedScrollView和RecyclerView,并且都是默认就支持发起嵌套滑动的,不需要我们手动调用setNestedScrollingEnabled方法去设置,在它们的构造方法中都会默认调用setNestedScrollingEnabled方法并传入true。到这里,文章一开始留下的疑问就解开了!
写在最后
关于Behavior的知识点其实远远不止这些,但是文章中的知识点足以帮助你对它有一个基本的认识了,如果想要更深入的了解它,还是要深入它的源码中去学习的。最后,我将文章中的一种重点总结在下面:
1.如果在xml中使用自定义Behavior,那么Behavior一定要实现默认的两个参数的构造方法。
2.如果Behavior所关联的View想要接受嵌套滑动,那么Behavior的onStartNestedScroll方法需要返回true,否则后续的几个和嵌套滑动相关的api方法都不会被调用。
3.CoordinatorLayout中的子View想要在任何API下都能发起嵌套滑动操作的前提是它实现了NestedScrollingChild接口,并且调用了setNestedScrollingEnabled方法来设置自身支持发起嵌套滑动。
最后的最后,还希望点赞支持一下,同时,有写的不对的地方虚心接受批评纠正!!!