前言
本章主要讲解下 CoordinatorLayout 的基础用法、工作原理和自定义Behavior
原理
使用很简单,百度上可以搜索下基础使用
协调者布局的功能
- 作为应用的顶层布局
- 作为一个管理容器,管理与子 View 或者子 View 之间的交互
- 处理子控件之间依赖下的交互
- 处理子控件之间的嵌套滚动
- 处理子控件的测量和布局
- 处理子控件的事件拦截与响应
以上 3、4、5、6的支持全部基于 CoordinatorLayout 中提供了一个叫作 Behavior 的插件,Behavior 内部也提供了相应的方法来对应这四个不同的功能;
对应关系如下
什么是 Behavior 插件
CoordinatorLayout 可以看做一个平台,在这个平台下的 ChildView 想要具备什么行为,就使用什么 Behavior(插件),集成不同的插件,实现不同的功能;
CoordinatorLayout 下依赖交互原理
当 CoordinatorLayout 中子控件 depandency 位置、大小发生改变的时候,那么在 CoordinatorLayout 内部会通知所有依赖 depandency 的控件,并调用对应声明的 Behavior,告知其依赖的 depandency 发生了改变。那么如何判断依赖(layoutDependsOn),接受到通知后如何处理(onDependentViewChanged/onDependentViewRemoved),这些都交给 Behavior 来处理;
layoutDependsOn
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
// 判断是不是依赖的 View
return dependency instanceof DependedView;
}
onDependentViewChanged
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
// 被依赖发生了变化,依赖的 child 做出相应的改变
child.setY(dependency.getBottom() + 50);
child.setX(dependency.getX());
return true;
}
CoordinatorLayout 下的嵌套滑动原理
CoordinatorLayout 实现了 NestedScrollingParent2 接口。那么当事件(scroll或fling)产生后,内部实现了 NestedScrollingChild 接口的子控件会将事件分发给 CoordinatorLayout,CoordinatorLayout 又会将事件传递给所有的 Behavior,然后在 Behavior 中实现子控件的嵌套滑动;
相当于 NestedScrolling 机制(参与角色只有子控件和父控件),CoordinatorLayout 中的交互玩出了新高度,在 CoordinatorLayout 下的子控件可以与多个兄弟控件进行交互;
CoordinatorLayout 下子控件的测量与布局
CoordinatorLayout 主要负责的是子控件之间的交互,内部控件的测量与布局其实非常简单,在特殊情况下,如子控件需要处理宽高和布局的时候,那么交给 Behavior 内部的 onMeasureChild、onChildLayout 方法进行处理;
CoordinatorLayout 下子控件的事件拦截
也是一样的处理逻辑,当 CoordinatorLayout 内部的 onTouchEvent、onInterceptTouchEvent 被调用的时候,如果子控件需要处理相关事件,会通过 Behavior 的对应的方法交给子 View 进行处理;
CoordinatorLayout 源码解析
View 的生命周期开始是从 onAttachToWindow 开始的,所以我们可以直接进入 CoordinatorLayout 的 onAttachToWinodw 方法看下:
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors(false);
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
// 关键点
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
//
...
// 省略部分代码
}
这里有一个比较关键的点 ViewTreeObserver ,调用它的 addOnPreDrawListener 添加了一个监听,这是一个视图树监听器,
ViewTreeObserver
注册一个观察者来监听视图树,当视图树的布局、焦点、绘制、滚动等发生改变的时候,ViewTreeObserver 都会收到通知,ViewTreeObserver 不能被实例化,可以调用 View.getViewTreeObserver() 来获得;
ViewTreeObserver.onPreDrawListener 当视图树将要被绘制的时候,回调 onPreDraw 接口;
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
这里面会调用 onChildViewsChanged 方法,我们进入这个方法看下,这个方法传入了一个 int 类型的 type,这个 type 有三种类型
static final int EVENT_PRE_DRAW = 0;
static final int EVENT_NESTED_SCROLL = 1;
static final int EVENT_VIEW_REMOVED = 2;
页面将要绘制的时候,传 0;
页面滚动的时候,传 1;
页面移除的时候,传 2;
也就说以上这三个状态都会调用 onChildViewChanged 方法的执行;
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();
// 遍历所有的子 View
for (int i = 0; i < childCount; i++) {
// 获取每一个子 View
final View child = mDependencySortedChildren.get(i);
//
...
// 省略部分代码
for (int j = i + 1; j < childCount; j++) {
// 获取依赖的 View
final View checkChild = mDependencySortedChildren.get(j);
// 获取这个 View 的布局参数
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
// 获取这 View 的 Behavior
final Behavior b = checkLp.getBehavior();
// 调用 layoutDependsOn 判断是不是要依赖的 View
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// 只是进行了状态重置
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// 移除的时候收到通知后的处理
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
// 滚动的时候收到通知后的处理
if (type == EVENT_NESTED_SCROLL) {
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
//
...
// 省略部分代码
}
看到这里,就能解释为什么在依赖的控件下设置一个Behavior,DependedView 位置发生改变的时候能通知到对方;
我们接下来进入这个获取 Behavior 的方法
final Behavior b = checkLp.getBehavior();
可以看到 Behavior 的初始化是在 LayoutParams 的构造方法中实例化的:
public static class LayoutParams extends MarginLayoutParams {
/**
* A {@link Behavior} that the child view should obey.
*/
Behavior mBehavior;
LayoutParams(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//
...
// 省略部分代码
// 实例化 Behavior
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
}
}
我们接着来看下 mDependencySortedChildren 是什么?
private final List<View> mDependencySortedChildren = new ArrayList<>();
它是一个集合,用来存放所有的子 View,那么问题来了,通过获取 View 不是通过 getChildAt(i) 来获取吗,这里为什么要多此一举在搞一个集合呢?
因为在 CoordinatorLayout 中,它管理的并不单单是一个 View 了,它管理是 View -> 依赖view 这样的一个关系,是一个 1:N 的关系图;
它还管理着这个图的数据结构
private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();
它管理的就是 childView -> dependency 的有向无环关系图,然后将这个关系图添加到 mDependencySortedChildren 中,我们可以来看下它俩是如何添加的
private void prepareChildren() {
mDependencySortedChildren.clear();
mChildDag.clear();
for (int i = 0, count = getChildCount(); i < count; i++) {
// 这里它会拿到每个 View
final View view = getChildAt(i);
// 获取每个 View 的布局参数
final LayoutParams lp = getResolvedLayoutParams(view);
lp.findAnchorView(this, view);
mChildDag.addNode(view);
for (int j = 0; j < count; j++) {
if (j == i) {
continue;
}
final View other = getChildAt(j);
// 寻找每个 View 的依赖关系
if (lp.dependsOn(this, view, other)) {
if (!mChildDag.contains(other)) {
// 添加到这个图数据结构中
mChildDag.addNode(other);
}
// 有向无环图需要一个边的概念
mChildDag.addEdge(other, view);
}
}
}
mDependencySortedChildren.addAll(mChildDag.getSortedList());
Collections.reverse(mDependencySortedChildren);
}
数据结构的概念这里先不详细讲解,感兴趣的后面单独写一篇,这里就是收集 View 获取依赖关系并保存到集合中;
Behavior 实战
国际惯例,先上效果:
布局实现如下:
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.example.llc.android_r.coordinatorlayout.DependencyView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="150dp"
android:layout_gravity="center"
android:text="我是科比"
android:textColor="@color/colorAccent"
app:layout_behavior=".coordinatorlayout.FollowBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
就是 TextView 跟随 DependencyView 的移动而移动
DependencyView 的实现如下:
public class DependencyView extends androidx.appcompat.widget.AppCompatImageView {
private float mLastX;
private float mLastY;
private final int mDragSlop;
public DependencyView(Context context) {
this(context, null);
}
public DependencyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DependencyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setImageResource(R.mipmap.kobe);
mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@SuppressLint("ClickableViewAccessibility")
@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;
}
}
一个简单的跟随手势移动而移动的自定义 ImageView
接下来我们来看下自定义 Behavior 的实现:
public class FollowBehavior extends CoordinatorLayout.Behavior<View> {
public FollowBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
return dependency instanceof DependencyView;
}
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
child.setX(dependency.getX());
child.setY(dependency.getY() + 200);
return true;
}
}
实现也比较简单,就是在 onDependentViewChanged 回调的时候修改依赖View的 X 和 Y 的坐标值,从而实现跟随移动;
原理,其他的例如颜色的跟随变动等等也是参考这样实现;
简历润色
深度理解 CoordinatorLayout 原理,并可以自定义 Behavior
下一章预告
自定义双指缩放的 PhotoView
欢迎三两
来都来了,点个关注,点个赞吧,你的支持是我最大的动力~