CoordinatorLayout:协调者布局

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

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

功能:

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

以上四个功能,都建立于 CoordainatorLayout中提供 的一个叫做Behavior的 “插件”之上。Behavior 内部也提供了相应方法来对 应这四个不同的功能

NestedScrolling机制的局限性:
child parent之间 1:1

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

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

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

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

这不就是观察者模式。


大家看这张图,由于我这边暂时没法录制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();
    }


    @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;
    }
}

观察者view1

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;
    }
}

观察者view2

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(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof DependedView;
    }

    @Override
    public 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.DependedView
        android:layout_width="80dp"
        android:layout_height="40dp"
        android:layout_gravity="center"
        android:background="#f00"
        android:gravity="center"
        android:textColor="#fff"
        android:textSize="18sp"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="跟随兄弟"
        app:layout_behavior=".coordinatorstudy.behavior.BrotherFollowBehavior"/>

    <TextView
        android: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 

在这里插入图片描述

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

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

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

同理:拦截情况

在这里插入图片描述
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 当视图树的触摸模式发生改变时,会调用的接口

@Override
    public 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 insets
            ViewCompat.requestApplyInsets(this);
        }
        mIsAttachedToWindow = true;
    }
 class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public 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,拿到子View
            final 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 anchor
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
				//过滤掉没有依赖关系的子View
                if (lp.mAnchorDirectChild == checkChild) {
                    offsetChildToAnchor(child, layoutDirection);
                }
            }

            // Get the current draw rect of the view
            getChildRect(child, true, drawRect);

            // Accumulate inset sizes
            if (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 necessary
            if (lp.dodgeInsetEdges != Gravity.NO_GRAVITY && child.getVisibility() == View.VISIBLE) {
                offsetChildByInset(child, inset, layoutDirection);
            }

            if (type != EVENT_VIEW_REMOVED) {
                // Did it change? if not continue
                getLastChildRect(child, lastDrawRect);
                if (lastDrawRect.equals(drawRect)) {
                    continue;
                }
                recordLastChildRect(child, drawRect);
            }

            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
            	//拿到子View
                final View checkChild = mDependencySortedChildren.get(j);
                //获取子View参数
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                //获取behavior
                final 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 graph
            for (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 added
                        mChildDag.addNode(other);
                    }
                    // Now add the dependency to the graph
                    mChildDag.addEdge(other, view);
                }
            }
        }

        // Finally add the sorted graph list to our list
        mDependencySortedChildren.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 that
        Collections.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() {
        }

        @Override
        public void onChildViewAdded(View parent, View child) {
            if (mOnHierarchyChangeListener != null) {
                mOnHierarchyChangeListener.onChildViewAdded(parent, child);
            }
        }

        @Override
        public 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相关嵌套方法。

找了一张总结的图
在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <TextView
        android: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.RecyclerView
        android: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);
    }

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, TextView child, int layoutDirection) {

        parent.onLayoutChild(child,layoutDirection);
        //初始滑动的时候实例化了一次,一开始是0,后面没再变过
        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;
    }

    /**
     * 当前方法逻辑其实很简单,头部的布局初始高度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
     */
    @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;
        //我们这个View的高度,固定的,这里是300
        int 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();
    }

    @Override
    public 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的变化
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
        return dependency instanceof TextView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View dependency) {
        //dependency是头部的view,如果它已经往上走的话,
        //dependency.getBottom()走之后的位置-child.getTop()原来的位置,是负数,则recyclerView也往上走这么多
        //反之亦然
        ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop()));
        return false;
    }
}

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值