当前位置: 代码迷 >> 综合 >> CoordinatorLayout:协调者布局
  详细解决方案

CoordinatorLayout:协调者布局

热度:109   发布时间:2023-10-10 14:24:43.0

如果还不了解NestedScroll机制的同学可以看:嵌套滑动

1、作为应用的顶层布局
2、作为一个管理容器,管理与子View或者子View之间的交互

功能:

  1. 处理子控件之间依赖下的交互
  2. 处理子控件之间的嵌套滑动
  3. 处理子控件的测量与布局
  4. 处理子控件的事件拦截与响应

以上四个功能,都建立于 CoordainatorLayout中提供 的一个叫做Behavior的 “插件”之上。Behavior 内部也提供了相应方法来对 应这四个不同的功能
CoordinatorLayout:协调者布局
NestedScrolling机制的局限性:
child parent之间 1:1

当CoordainatorLayout中子控件depandency的位置、大小等发生改变的时候,那么在

CoordainatorLayout内部会通知所有依赖depandency的控件,

并调用对应声明的Behavior,告知其依赖的depandency发生改变。

那么如何判断依赖(layoutDependsOn),接受到通知后如何处理(onDependentViewChanged/onDepe ndentViewRemoved),这些都交由Behavior来处理。

这不就是观察者模式。

CoordinatorLayout:协调者布局
大家看这张图,由于我这边暂时没法录制gif图,直接就截图了一张,比较简单还是能说清楚的。

当我鼠标移动的时候下面的view跟随移动,最上面的那个view改变颜色。

被观察者的view

public class DependedView extends View {
    private float mLastX;private float mLastY;private final int mDragSlop;public DependedView(Context context) {
    this(context, null);}public DependedView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);}public DependedView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();}@Overridepublic 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;}
}

观察者view1

public class BrotherFollowBehavior extends CoordinatorLayout.Behavior<View> {
    public BrotherFollowBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);}@Overridepublic boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof DependedView;}@Overridepublic boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    child.setY(dependency.getBottom() + 50);child.setX(dependency.getX());return true;}
}

观察者view2

public class BrotherChameleonBehavior extends CoordinatorLayout.Behavior<View> {
    private ArgbEvaluator mArgbEvaluator = new ArgbEvaluator();public BrotherChameleonBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);}@Overridepublic boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof DependedView;}@Overridepublic boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    int color = (int) mArgbEvaluator.evaluate(dependency.getY() / parent.getHeight(), Color.WHITE, Color.BLACK);child.setBackgroundColor(color);return false;}
}

xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".coordinatorstudy.Demo01Activity"><com.zero.materialdesign.coordinatorstudy.view.DependedViewandroid:layout_width="80dp"android:layout_height="40dp"android:layout_gravity="center"android:background="#f00"android:gravity="center"android:textColor="#fff"android:textSize="18sp"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="跟随兄弟"app:layout_behavior=".coordinatorstudy.behavior.BrotherFollowBehavior"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="变色兄弟"app:layout_behavior=".coordinatorstudy.behavior.BrotherChameleonBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

很明显能观察到想要观察的view:extends CoordinatorLayout.Behavior
layoutDependsOn–》判断该子View是我想要观察的view
onDependentViewChanged–》做出你自己想要的逻辑变化

通过上面的例如大家应该知道它是一对多进行通知的。

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

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,NestedScrollingParent3 

CoordinatorLayout:协调者布局

1.产生事件(scroll或fling)的控件必须是CoordinatorLayout的直接子View 吗?
不是,从嵌套滑动那一篇中我们知道嵌套滑动机制是递归查询父View的。

2.响应Behavior的控件必须是CoordinatorLayout的直接子View吗?
是的,因为它只收集了它的直接子类的之间的关系

测量与布局:
CoordainatorLayout主要负责的是子控件之间的交互,内部控件的测量与布局,都非常简单。在特殊的情况下,如子控件需要处理宽高和布局的时候,那么交由Behavior内部的onMeasureChild与onLayoutChild方法来进行处理
CoordinatorLayout:协调者布局
同理:拦截情况

CoordinatorLayout:协调者布局
1、为什么在依赖的控件下设置一个behavior,DepandedView位置发生改变的时候就能通知依赖方?
2、Behavior是在哪儿实例化的?
3、CoordinatorLayout是如何区分谁依赖于谁的?
4、onMeasure收集什么时候需要重写onMeasureChild?
5、什么时候需要重写onLayoutChild?

view的生命周期从onAttachedToWindow开始,那么我们就从CoordinatorLayout的对应这个方法找起。

先了解一下:ViewTreeObserver

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

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

ViewTreeObserver常用内部类:

内部类接口 备注
ViewTreeObserver.OnPreDrawListener 当视图树将要被绘制时,会调用的接口
ViewTreeObserver.OnGlobalLayoutListener 当视图树的布局发生改变或者View在视图树的可见状态发生改变时会调用的接口
ViewTreeObserver.OnGlobalFocusChangeListener 当一个视图树的焦点状态改变时,会调用的接口
ViewTreeObserver.OnScrollChangedListener 当视图树的一些组件发生滚动时会调用的接口
ViewTreeObserver.OnTouchModeChangeListener 当视图树的触摸模式发生改变时,会调用的接口

@Overridepublic void onAttachedToWindow() {
    super.onAttachedToWindow();resetTouchBehaviors(false);if (mNeedsPreDrawListener) {
    if (mOnPreDrawListener == null) {
    mOnPreDrawListener = new OnPreDrawListener();}final ViewTreeObserver vto = getViewTreeObserver();//添加监听vto.addOnPreDrawListener(mOnPreDrawListener);}if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
    // We're set to fitSystemWindows but we haven't had any insets yet...// We should request a new dispatch of window insetsViewCompat.requestApplyInsets(this);}mIsAttachedToWindow = true;}
 class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
    @Overridepublic boolean onPreDraw() {
    //绘制之前onChildViewsChanged(EVENT_PRE_DRAW);return true;}}

总共三个标志:
static final int EVENT_PRE_DRAW = 0; //绘制之前
static final int EVENT_NESTED_SCROLL = 1;//嵌套滑动之前
static final int EVENT_VIEW_REMOVED = 2;//移除之前
都会调用onChildViewsChanged

凡是源码只需要看有中文注释的地方,跟着思路走

    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();for (int i = 0; i < childCount; i++) {
    //跟我们view三部曲中获取子View:getChildAt() 不一样//mDependencySortedChildren是一个列表,里面存储了所有的子view,拿到子Viewfinal View child = mDependencySortedChildren.get(i);final LayoutParams lp = (LayoutParams) child.getLayoutParams();if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
    // Do not try to update GONE child views in pre draw updates.continue;}// Check child views before for anchorfor (int j = 0; j < i; j++) {
    final View checkChild = mDependencySortedChildren.get(j);//过滤掉没有依赖关系的子Viewif (lp.mAnchorDirectChild == checkChild) {
    offsetChildToAnchor(child, layoutDirection);}}// Get the current draw rect of the viewgetChildRect(child, true, drawRect);// Accumulate inset sizesif (lp.insetEdge != Gravity.NO_GRAVITY && !drawRect.isEmpty()) {
    final int absInsetEdge = GravityCompat.getAbsoluteGravity(lp.insetEdge, layoutDirection);switch (absInsetEdge & Gravity.VERTICAL_GRAVITY_MASK) {
    case Gravity.TOP:inset.top = Math.max(inset.top, drawRect.bottom);break;case Gravity.BOTTOM:inset.bottom = Math.max(inset.bottom, getHeight() - drawRect.top);break;}switch (absInsetEdge & Gravity.HORIZONTAL_GRAVITY_MASK) {
    case Gravity.LEFT:inset.left = Math.max(inset.left, drawRect.right);break;case Gravity.RIGHT:inset.right = Math.max(inset.right, getWidth() - drawRect.left);break;}}// Dodge inset edges if necessaryif (lp.dodgeInsetEdges != Gravity.NO_GRAVITY && child.getVisibility() == View.VISIBLE) {
    offsetChildByInset(child, inset, layoutDirection);}if (type != EVENT_VIEW_REMOVED) {
    // Did it change? if not continuegetLastChildRect(child, lastDrawRect);if (lastDrawRect.equals(drawRect)) {
    continue;}recordLastChildRect(child, drawRect);}// Update any behavior-dependent views for the changefor (int j = i + 1; j < childCount; j++) {
    //拿到子Viewfinal View checkChild = mDependencySortedChildren.get(j);//获取子View参数final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();//获取behaviorfinal Behavior b = checkLp.getBehavior();//behavior不等于null,且是依赖的view则进入判断//checkChild依赖方(观察者),child(被观察者)if (b != null && b.layoutDependsOn(this, checkChild, child)) {
    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
    // If this is from a pre-draw and we have already been changed// from a nested scroll, skip the dispatch and reset the flag//绘制之前打个标记checkLp.resetChangedAfterNestedScroll();continue;}final boolean handled;switch (type) {
    case EVENT_VIEW_REMOVED:// EVENT_VIEW_REMOVED means that we need to dispatch// onDependentViewRemoved() instead//被移除时调用b.onDependentViewRemoved(this, checkChild, child);handled = true;break;default:// Otherwise we dispatch onDependentViewChanged()//其他情况走这里handled = b.onDependentViewChanged(this, checkChild, child);break;}if (type == EVENT_NESTED_SCROLL) {
    // If this is from a nested scroll, set the flag so that we may skip// any resulting onPreDraw dispatch (if needed)//当嵌套滑动时还会走这里checkLp.setChangedAfterNestedScroll(handled);}}}}releaseTempRect(inset);releaseTempRect(drawRect);releaseTempRect(lastDrawRect);}

两个局部变量列表,一个是存储所有子View的列表,一个是存储子View之间依赖关系的列表。

private final List<View> mDependencySortedChildren = new ArrayList<>();private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();

DirectedAcyclicGraph:图的数据结构
类似haspMap,有向无环图
临接表 = 数据+嵌套链表
这里几乎是纯数据结构的知识,不懂也没关系,知道这回事就行了。
1:N 关系
在onMeasure绘制的时候清空再重新存储赋值,度量的时候会先调用下面这个方法

private void prepareChildren() {
    mDependencySortedChildren.clear();mChildDag.clear();for (int i = 0, count = getChildCount(); i < count; i++) {
    final View view = getChildAt(i);final LayoutParams lp = getResolvedLayoutParams(view);lp.findAnchorView(this, view);mChildDag.addNode(view);// Now iterate again over the other children, adding any dependencies to the graphfor (int j = 0; j < count; j++) {
    if (j == i) {
    continue;}final View other = getChildAt(j);if (lp.dependsOn(this, view, other)) {
    if (!mChildDag.contains(other)) {
    // Make sure that the other node is addedmChildDag.addNode(other);}// Now add the dependency to the graphmChildDag.addEdge(other, view);}}}// Finally add the sorted graph list to our listmDependencySortedChildren.addAll(mChildDag.getSortedList());// We also need to reverse the result since we want the start of the list to contain// Views which have no dependencies, then dependent views after thatCollections.reverse(mDependencySortedChildren);}

重新度量绘制的时候会调用LayoutParams

//重写LayoutParams
public static class LayoutParams extends MarginLayoutParams
if (mBehaviorResolved) {
    //通过反射去实例化Behavior mBehavior = parseBehavior(context, attrs, a.getString(R.styleable.CoordinatorLayout_Layout_layout_behavior));}

在CoordinatorLayout的构造函数里面:监听所有子View的情况

 super.setOnHierarchyChangeListener(new HierarchyChangeListener());
 private class HierarchyChangeListener implements OnHierarchyChangeListener {
    HierarchyChangeListener() {
    }@Overridepublic void onChildViewAdded(View parent, View child) {
    if (mOnHierarchyChangeListener != null) {
    mOnHierarchyChangeListener.onChildViewAdded(parent, child);}}@Overridepublic void onChildViewRemoved(View parent, View child) {
    onChildViewsChanged(EVENT_VIEW_REMOVED);if (mOnHierarchyChangeListener != null) {
    mOnHierarchyChangeListener.onChildViewRemoved(parent, child);}}}

监听view的移除和创建

在它的内部:onInterceptTouchEvent和onTouchEvent方法的时候会调用所有的有behavior的子View的对应onInterceptTouchEvent和onTouchEvent,当然一般情况下子View都不会去拦截。

它是一个Viewgroup,大部分情况下会把事件传递给实现了NestedScrollingChild的子View,基本上是recyclerView,然后传递给CoordinatorLayou的相关嵌套方法,之后再调用子View的behavior相关嵌套方法。

找了一张总结的图
CoordinatorLayout:协调者布局

下面看一个实例:(我的gif图还是上传失败了,好气!)

CoordinatorLayout:协调者布局
CoordinatorLayout:协调者布局
CoordinatorLayout:协调者布局

内容往上走的时候topView隐藏,当往下走的时候优先显示topView之后才走RecyclerView
可以看到最后一张图,topView先完全显示再走recyclerView

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:layout_width="match_parent"android:layout_height="100dp"android:background="@color/colorPrimaryDark"android:gravity="center"android:text="Behavior的嵌套滑动展示"android:textColor="#fff"app:layout_behavior=".coordinatorstudy.simplebehavior.SampleHeaderBehavior"/><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recycler_view"android:layout_width="match_parent"android:layout_height="match_parent"app:layout_behavior=".coordinatorstudy.simplebehavior.ScrollerBehavior"/></androidx.coordinatorlayout.widget.CoordinatorLayout>

//没有重写layoutDependsOn,TextView没有依赖recyclerView
//它是依靠NestedScroll机制来变化的,所以下面直接ViewCompat.offsetTopAndBottom
public class SampleHeaderBehavior extends CoordinatorLayout.Behavior<TextView> {
    private int mOffsetTopAndBottom;private int mLayoutTop;public SampleHeaderBehavior() {
    }public SampleHeaderBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);}@Overridepublic boolean onLayoutChild(CoordinatorLayout parent, TextView child, int layoutDirection) {
    parent.onLayoutChild(child,layoutDirection);//初始滑动的时候实例化了一次,一开始是0,后面没再变过mLayoutTop = child.getTop();return true;}@Overridepublic boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
    return true;}/*** 当前方法逻辑其实很简单,头部的布局初始高度300,屏幕内容往上走的时候dy是正值,* 大家都知道屏幕的圆点在左上角,scrollBar往下走是Y是增加的,但是屏幕是内容是往上走。* 反之亦然,下面的逻辑:* 屏幕内容往上走top内容要隐藏,最多走300,mOffsetTopAndBottom初始值肯定是0,* mOffsetTopAndBottom - dy;dy是正值,开始是负数,(-dy其实就是负数在增加)加上mOffsetTopAndBottom,* offset = offset < minOffset ? minOffset : (offset > maxOffset ? maxOffset : offset);*上面这个判断如果还在-300~0之间的话,一直是大于minOffset,小于maxOffset(0),去它自己* int top = child.getTop();从0到-300,* int lastOffset = offset - (top - mLayoutTop); 过程中top从0到负数* 例如:-200-(-5(之间已经滑动了5)),当前最大可以滑动195,这个数字是dy传过来的最大值* 如果是传递过来一个800,-805小于-300,取-300,已经滑了5,那么最大滑295** 如果是屏幕内容往下走,topView要先显示出来,那么dy就是负值,mOffsetTopAndBottom - dy;* 例如:-200-(dy==-30),其实就是数字在增加,* mOffsetTopAndBottom已经是-300* offset = offset < minOffset ? minOffset : (offset > maxOffset ? maxOffset : offset);* 如果是先滑5,那么-300-(-5)=-295,大于-300,小于0,还是取它自己-295* int lastOffset = offset - (top - mLayoutTop); top开始从-300计算* -295-(-300) = 5,其实就是减去已经滑了多少,下面就是-295** 首先要明白:childView.getHeight();是一个固定值,代表view自身的高度* child.getTop();是一个变化值,从0到-300,再从-300到0** mOffsetTopAndBottom记录滑了多少,child.getTop();也算是可以得出滑了多少,* 一个局部变量记录,一个每次都动态获取,双重保障** scrollTo、scrollY这些可以看做是translate的动画平移** 而ViewCompat.offsetTopAndBottom是真正的改变了view的位置属性,可以看做是属性动画* 往上走传递负数,往下走传递正值** 这个方法我看到网上还有一些比较简单的写法,其实都可以,有很多写法,原理都一致。** @param coordinatorLayout* @param child* @param target* @param dx* @param dy* @param consumed* @param type*/@Overridepublic 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;//我们这个View的高度,固定的,这里是300int minOffset = -getChildScrollRang(child);int maxOffset = 0;offset = offset < minOffset ? minOffset : (offset > maxOffset ? maxOffset : offset);int top = child.getTop();//在屏幕坐标系的相对位置int lastOffset = offset - (top - mLayoutTop);ViewCompat.offsetTopAndBottom(child, lastOffset);consumedy = mOffsetTopAndBottom - offset;// 将本次滚动到的位置记录下来mOffsetTopAndBottom = offset;consumed[1] = consumedy;}// 获取childView最大可滑动距离private int getChildScrollRang(View childView) {
    if (childView == null) {
    return 0;}return childView.getHeight();}@Overridepublic boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View target, float velocityX, float velocityY) {
    return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);}
}
public class ScrollerBehavior extends CoordinatorLayout.Behavior<RecyclerView> {
    public ScrollerBehavior() {
    }public ScrollerBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);}//很明显recyclerView依赖于TextView的变化@Overridepublic boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
    return dependency instanceof TextView;}@Overridepublic boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View dependency) {
    //dependency是头部的view,如果它已经往上走的话,//dependency.getBottom()走之后的位置-child.getTop()原来的位置,是负数,则recyclerView也往上走这么多//反之亦然ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop()));return false;}
}

谷歌官方自带的效果这边就不介绍了,我一开始是从郭霖大神的第一行代码那里看到比较详细的介绍,后来也在网上看了一些,都写得挺好的。