CoordinatorLayout的功能

事先警示:CoordinatorLayout只继承了NestedScrollParent,也就是说它只能做顶层父View,别踩坑!!!

  1. 处理子控件之间依赖下的交互

  2. 处理子控件之间的嵌套滑动

  3. 处理子控件的测量与布局

  4. 处理子控件的事件拦截与响应

    以上四个功能,都建立与CoordinatorLayout中提供的一个叫做Behavior的“插件”之上。Behavior内部也提供了相应方法来对应这四个不同的功能。
    Behavior内部集成了上述四种功能对应的方法,实现解耦。

image-20220806114929932

什么是Behavior?
可以把Behavior理解成插件,当我们的组件想实现什么样的功能的时候就使用对应的Behavior,也就是说很多种不同功能的Behavior,当然也可以自定义Behavior。

CoordinatorLayout下依赖交互原理

image-20220806115038051

当CoordinatorLayout中子控件dependency的位置、大小发生变化的时候,那么在CoordinatorLayout内部会通知所有依赖depandency的控件,并调用对应声明的Behavior,告知其依赖的dependency发生变化。
那么如何判断依赖->layoutDependsOn
接收到通知后如何处理->onDependentViewChanged/onDependentViewRemoved
这些都是由Behavior来处理。

主要方法介绍

  • layoutDependsOn(CoordinatorLayout parent, View child, View dependency) 表示是否给应用了Behavior 的View 指定一个依赖的布局

    • 参数1:coordinatorlayout对象
    • 参数2:child 被观察的View
    • 参数3:依赖变化的View(被观察的View)
  • onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) 当依赖的View发生变化的时候hi掉的方法

  • onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) 当用户手指按下的时候,你是否要处理这次操作。当你确定要处理这次操作的时候,返回true;如果返回false的时候,就不会去响应后面的回调事件了。你想怎么滑就怎么话,我都不做处理。这里的(axes)滚动方向很重要,可以通过此参数判断滚动方向!

    • 参数3:直接目标,相当于能滑动的控件
    • 参数4:观察的View
    • 参数5:这个可以简单理解为滚动方向
      • ViewCompat#SCROLL_AXIS_HORIZONTAL 水平方向
      • ViewCompat#SCROLL_AXIS_VERTICAL 竖直方向
    • 参数6:这个参数是之后有的,如果你输入的类型不是TYPE_TOUCH那么就不会相应这个滚动
  • onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type) 当onStartNestedScroll准备处理这次滑动的时候(返回true的时候),回调这个方法。可以在这个方法中做一些响应的准备工作!

  • onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type) 当滚动开始执行的时候回调这个方法。

    • 参数4/参数5:用户x/y轴滚动的距离(注意这里是每一次都回调的啊!!!)
    • 参数6:处理滚动的距离的参数,内部维护着输出距离,假设用户滑动了100px,child 做了90px的位移,你需要把consumed[1]的值改成90,这样coordinatorLayout就能知道只处理剩下的10px的滚动。其中consumed[0]代表x轴、consumed[1]代表y轴。可能你不理解这个问题,换个形象点的比喻,比如你开发某一个功能,但是你只会其中的90%那么怎么办呢?不能就不管了。好你找到了你的同事或者老大,让他去完成剩下的10%。这样问题就完美的解决了,是一个概念的!
  • onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) 上面这个方法结束的时候,coordinatorLayout处理剩下的距离,比如还剩10px。但是coordinatorLayout发现滚动2px的时候就已经到头了。那么结束其滚动,调用该方法,并将coordinatorLayout处理剩下的像素数作为参(dxUnconsumed、dyUnconsumed)传过来,这里传过来的就是 8px。参数中还会有coordinatorLayout处理过的像素数(dxConsumed、dyConsumed)。老大开始处理剩下的距离了!这个方法主要处理一些越界后的滚动。还是不懂对吧!还拿你们老大做比喻:比如上面还剩 10%的工作,这时老大处理了2%后发现已经可以上线了,于是老大结束了工作,并将处理剩下的内容(dxUnconsumed、dyUnconsumed)纪录下来,告诉你。老大处理了的内容(dxConsumed、dyConsumed)也告诉了你。

    • 参数4/参数5:当没有滚动到顶部或者底部的时候,x/y轴的滚动距离
    • 参数6/参数7:当滚动到顶部或者底部的时候,x/y轴的滚动距离
  • onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY) 当手指松开发生惯性动作之前调用,这里提供了响应的速度,你可以根据速度判断是否需要进行折叠等一系列的操作,你要确定响应这个方法的话,返回true。

    • 参数4/参数5:代表相应的速度
  • onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int type) 停止滚动的时候回调的方法。当你不去响应Fling的时候会直接回调这个方法。在这里可以做一些清理工作。或者其他的内容。。。

  • onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) 确定子View位置的方法,这个方法可以重新定义子View的位置(这里明确是设置behavior的那个View哦),例如下面这样

    • ViewCompat#LAYOUT_DIRECTION_LTR 视图方向从左到右
    • ViewCompat#LAYOUT_DIRECTION_RTL 视图方向从优到左

CoordinatorLayout下嵌套滑动原理

image-20220806115051276

CoordinatorLayout实现了NestedScrollingParent2接口。那么当事件(scroll或fling)产生后,内部实现了NestedScrollingChild接口的子控件会将事件分发给CoordinatorLayout,CoordinatorLayout又会将事件传递给所有的Behavior中实现子控件的嵌套滑动。

image-20220806142237581

相对于NestedScrollView机制(参数角色只有子控件和父控件),CoordinatorLayout中的交互角色玩出了新高度,在CoordinatorLayout下的子控件可以与多个兄弟控件进行交互。

image-20220806115109848

大家可以看一下整个事件的流程图

image-20220806144653508

浅谈View的生命周期

image-20220806115156782

层级关系:activity->window->view
LayoutInflater.inflate将view解析出来
在onFinishInflate阶段add到content中,content属于DecorView
在Activity中通过attach方法new一个PhoneWindow
在resume阶段通过makevisible方法PhoneWindow.addView(DecorView)
再回调onAttachToWindow,然后已经一系列方法(包括setView->WMS…开始绘制)才呈现出View

源码实现

下面是截取的重要源码截图

ViewTreeObserver介绍

ViewTreeObserver注册一个观察者来监听视图树,当是图书的布局、视图树的焦点、视图将要绘制、视图滚动等发生改变时,ViewTreeObserver都会收到通知,ViewTreeObsrver不能被实例化,可以调用,View.getViewTreeObserver()来获得

dispatchOnOreDraw():通知观察者绘制即将开始,如果其中的某个观察者返回true,那么绘制会取消,并且重新安排绘制,如果想在View Layout或View hierarchy还未依附到Window时,或者在View处于CONE状态时强制绘制,可以手动调用这个方法。
通过ViewTreeObserver添加绘制监听

image-20220806115229067

注意,这里不是getChildAt()而是mDependencySortedChildren.get(),因为mDependencySortedChildren在这里是一个特殊的集合,因为CoordinatorLayout管理着多个子View,并且子View之间还存在依赖关系,所以还利用了有向无环图来记录View的依赖关系。只需理解原因即可,这里不用深究,否则会陷到坑里面去。
image-20220806115313761
image-20220806115333029

image-20220806115712870

Behavior在哪里被实例化?
image-20220806115344751

如果自定义ViewGroup并且需要提供一些特殊的属性给子View,那么就需要重写LayoutParams

image-20220806115427736
image-20220806115444264

HierarchyChangeListener回调监听,当childView添加与删除时调用

image-20220806115502591

image-20220806115532076

前面提到了mDependencySortedChildren,那么mDependencySortedChildren在哪初始化呢?

image-20220806115554255
image-20220806115611892
image-20220806115633713

Demo

效果展示

这是利用Behavior实现的嵌套滑动效果,上滑时顶部会隐藏,往下滑时顶部又会回显出来

录制_2022_08_06_15_07_40_12

public class SampleHeaderBehavior extends CoordinatorLayout.Behavior<TextView> {
private int mOffsetTopAndBottom;
private int mLayoutTop;
public SampleHeaderBehavior(){

}
public SampleHeaderBehavior(Context context, AttributeSet attrs){
super(context,attrs);
}

@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull TextView child, int layoutDirection) {
parent.onLayoutChild(child,layoutDirection);
mLayoutTop = child.getTop();
return true;
}

@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return true;
}


@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
int consumedy = 0;//记录消费的距离
int offset = mOffsetTopAndBottom - dy;
//因为offset是带方向的,这里获取到最大可滑动距离需要在前面添加负号
int minOffset = -getChildScrollRange(child);
int maxOffset = 0;
offset = offset<minOffset? minOffset:(Math.min(offset, maxOffset));
ViewCompat.offsetTopAndBottom(child,offset-(child.getTop()-mLayoutTop));
consumedy = mOffsetTopAndBottom - offset;
//将本次滑动到的位置记录下来
mOffsetTopAndBottom = offset;
consumed[1] = consumedy;
}

//获取childView最大可滑动距离
private int getChildScrollRange(View childView){
if (childView==null){
return 0;
}
return childView.getHeight();
}
}
------------------------------------------------------------------------------------------------------------
public class ScrollerBehavior extends CoordinatorLayout.Behavior<RecyclerView> {
public ScrollerBehavior() {
}

public ScrollerBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull RecyclerView child, @NonNull View dependency) {
return dependency instanceof TextView;
}

@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull RecyclerView child, @NonNull View dependency) {
ViewCompat.offsetTopAndBottom(child,dependency.getBottom()-child.getTop());
return false;
}
}

上面demo其实是对前面提到的嵌套滑动方法的实践。而这个例子才是对依赖与被依赖组件关系的效果展示,红色方块是被依赖方,而“跟随兄弟”和变色兄弟都是依赖方,会根据红色方块的位置变化而变化。

录制_2022_08_06_15_09_45_364

public class DependedView extends View {
private float mLastX;
private float mLastY;
private int mDragSlop;
public DependedView(Context context, int mDragSlop) {
super(context);
this.mDragSlop = mDragSlop;
}

public DependedView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

public DependedView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
mLastY = event.getY();
break;

case MotionEvent.ACTION_MOVE:
int dx = (int) (event.getX() - mLastX);
int dy = (int) (event.getY() - mLastY);
if (Math.abs(dx) > mDragSlop || Math.abs(dy) > mDragSlop) {
ViewCompat.offsetTopAndBottom(this, dy);
ViewCompat.offsetLeftAndRight(this, dx);
}
mLastX = event.getX();
mLastY = event.getY();
break;

default:
break;

}

return true;
}
}
------------------------------------------------------------------------------------------------------------
public class BrotherChameleonBehavior extends CoordinatorLayout.Behavior<View> {
private ArgbEvaluator mArgbEvaluator = new ArgbEvaluator();
public BrotherChameleonBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
return dependency instanceof DependedView;
}

@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
int color = (int)mArgbEvaluator.evaluate(dependency.getY()/parent.getHeight(), Color.WHITE,Color.BLACK);
child.setBackgroundColor(color);
return false;
}
}
------------------------------------------------------------------------------------------------------------
public class BrotherFollowBehavior extends CoordinatorLayout.Behavior<View> {
public BrotherFollowBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof DependedView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
child.setY(dependency.getBottom() + 50);
child.setX(dependency.getX());
return true;
}
}

总结

用好Behavior对我们做好看的UI效果有很大的帮助,毕竟NestedScrollParent和NestedScrollChild也只能实现两个组件之间的嵌套关系,而Behavior能够实现一对多的嵌套关系。这节内容主要是对CoordinatorLayout的介绍以及只是对自定义Behavior的浅尝试,后面小编将写一篇真正意义的自定义Behavior的Demo介绍与实现。

代码链接