Material Design之CoordinatorLayout原理剖析

此篇文章需要用到之前的知识点,即我之前写的这两篇博客
事件分发机制原理分析
NestedScrolling机制原理分析

一.CoordinatorLayout基本介绍

1.使用场景

一般作为应用的顶层布局,同时也作为一个管理容器,管理与子view 或者 view之间的交互。那么都具体管理啥呢?可以分成四个部分

2.作用

  • 处理子控件之间依赖下的交互
  • ②处理子控件之间的嵌套滑动
  • ③处理子控件的测量与布局
  • ④处理子控件的事件拦截与响应

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

3.Behavior中的常用方法

Material Design之CoordinatorLayout原理剖析

4.为什么把这些方法都放到Behavior中呢?

答案:解耦。当用的时候就集成进去,不用的时候就remove。即可插拔

  • 在这里可以把CoordinatorLayout比喻成Android Studio,把子View比喻成我们的项目。我们知道,AS可以使用Plugins为项目引入各种插件,从而实现不同的功能,同样的道理,CoordinatorLayout可以使用Behavior为子View引入各种行为。Behavior也是**“可插拔”**

二.CoordinatorLayout四个功能的原理

1.CoordinatorLayout下依赖交互功能原理(观察者模式)

CoordainatorLayout中子控件depandency的位置、大小等发生改变的时候,那么在CoordainatorLayout内部会通知所有依赖depandency的控件,并调用对应声明的Behavior,告知其依赖的depandency发生改变。

  • 那么如何判断依赖是哪个View呢?
    layoutDependsOn方法
  • 接受到通知后如何处理呢?
    onDependentViewChanged/onDependentViewRemoved方法

原理图
Material Design之CoordinatorLayout原理剖析
dependency也是一个child,和child1child2在布局上可以是并列的。后面我们把dependency统一称为DepandedView

2.CoordinatorLayout内部嵌套滑动原理

CoordinatorLayout实现了NestedScrollingParent2接口。所以当事件(scrollfling)产生后,内部实现了NestedScrollingChild接口的子控件会将事件传递给CoordinatorLayoutCoordinatorLayout又会将事件传递给所有的Behavior。然后在Behavior中实现子控件的嵌套滑动。
Material Design之CoordinatorLayout原理剖析
具体流程图
Material Design之CoordinatorLayout原理剖析

相对于NestedScrolling机制(参与角色只有子控件和父控件),CoordainatorLayout中的交互角色玩出了新高度,在CoordainatorLayout下的子控件可以与多个兄弟控件进行交互。即从1:1变成了1:N

3.CoordinatorLayout子控件的测量与布局

在特殊的情况下,如子控件需要处理宽高和布局的时候,那么交由Behavior内部的onMeasureChildonLayoutChild方法来进行处理
Material Design之CoordinatorLayout原理剖析

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

对于事件的拦截与处理,如果子控件需要拦截并消耗事件,那么交由给Behavior内部的onInterceptTouchEventonTouchEvent方法进行处理
Material Design之CoordinatorLayout原理剖析

三.源码分析CoordinatorLayout子控件依赖交互功能原理

由于View的生命周期的开始是在onAttachedToWindow方法中,所以我们进入此方法寻找

1.我们在CoordinatorLayout类中找到onAttachedToWindow方法

发现它调用getViewTreeObserver,获得ViewTreeObserver,然后调用了addOnPreDrawListener
Material Design之CoordinatorLayout原理剖析

  • 关于ViewTreeObserver
    ViewTreeObserver 注册一个观察者来监听视图树,当视图树的布局、视图树的焦点、视图树将要绘制、视图树滚动等发生改变时,ViewTreeObserver都会收到通知,ViewTreeObserver不能被实例化,可以调用View.getViewTreeObserver()来获得
  • 关于dispatchOnPreDraw:通知观察者绘制即将开始,如果其中的某个观察者返回true,那么绘制将会取消,并且重新安排绘制,如果想在View LayoutViewhierarchy 还未依附到Window时,或者在View处于GONE状态时强制绘制,可以手动调用这个方法
2.接下来我们看一下它添加的监听者是个啥,追踪addOnPreDrawListener,找到OnPreDrawListener

Material Design之CoordinatorLayout原理剖析
View发生变化的时候会调用onChildViewsChanged方法。

3.我们追踪onChildViewsChanged

它有一个类型,即type
Material Design之CoordinatorLayout原理剖析
也就是DispatchChangeEvent注解。我们点进去看一下
Material Design之CoordinatorLayout原理剖析
一个代表绘制之前,一个代表嵌套滑动,一个代表View移除。意思是说这三种类型的事件发生的时候会调用onChildViewsChanged方法。

我们深究一下这个方法

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        。。。
        //--------------------------------------
        //--------------------------------------
        //得到子View的数目
        //--------------------------------------
        //--------------------------------------
        final int childCount = mDependencySortedChildren.size();
        。。。
		//--------------------------------------
        //--------------------------------------
        //取出每一个子View,即child 
        //--------------------------------------
        //--------------------------------------
        for (int i = 0; i < childCount; i++) {
            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;
            }
			。。。
            //--------------------------------------
	        //--------------------------------------
	        //然后再嵌套一个for循环,再依次重新取出子View
	        //--------------------------------------
	        //--------------------------------------
            for (int j = i + 1; j < childCount; j++) {
            	//--------------------------------------
		        //--------------------------------------
		        //首先得到子View即checkChild ,然后得到子View的LayoutParams,再根据它得到Behavior 
		        //--------------------------------------
		        //--------------------------------------
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();
				//--------------------------------------
		        //--------------------------------------
		        //判断一下此时得到的checkChild是不是要依赖child
		        //换句话说,看一下这个child是不是被依赖的
		        //--------------------------------------
		        //--------------------------------------
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        //--------------------------------------
				        //--------------------------------------
				        //如果type是EVENT_PRE_DRAW(这个type上面说过),则调用resetChangedAfterNestedScroll
				        //--------------------------------------
				        //--------------------------------------
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            //--------------------------------------
					        //--------------------------------------
					        //如果type是EVENT_VIEW_REMOVED,则调用onDependentViewRemoved
					        //--------------------------------------
					        //--------------------------------------
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            //--------------------------------------
					        //--------------------------------------
					        //如果type是其他类型,则调用onDependentViewChanged
					        //--------------------------------------
					        //--------------------------------------
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    if (type == EVENT_NESTED_SCROLL) {
                        //--------------------------------------
				        //--------------------------------------
				        //如果type是EVENT_NESTED_SCROLL,则调用setChangedAfterNestedScroll
				        //--------------------------------------
				        //--------------------------------------                        		
				        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }
		...
    }

通过上面的源码,我们了解了在需要依赖其他控件的控件中设置一个behavior,那么被依赖的控件也就是DepandedView发生变化的时候就能通知对方的原因,主要是onChildViewsChanged方法。

  • 那么为什么可以调用remove相应的方法(即onDependentViewRemoved)呢?
    ①我们在CoordinatorLayout的构造方法中发现它调用了setOnHierarchyChangeListener
    Material Design之CoordinatorLayout原理剖析
    我们看它传入的参数是
    Material Design之CoordinatorLayout原理剖析
    ②追踪进入,发现HierarchyChangeListener类实现了OnHierarchyChangeListener接口。
    Material Design之CoordinatorLayout原理剖析
    ③我们再追踪OnHierarchyChangeListener
    Material Design之CoordinatorLayout原理剖析
    发现它是一个接口,定义了两个方法。
    EVENT_VIEW_REMOVED类型事件发生的时候,就会调用onChildViewsChanged方法,然后参数传入EVENT_VIEW_REMOVED,然后就会回调BehavioronDependentViewRemoved,也就是上面源码分析的那部分
    Material Design之CoordinatorLayout原理剖析

四.源码总结

总结一波:
CoordinatorLayout的某一DepandedView发生变化的时候,必然会导致重绘,然后就会调用
onChildViewsChanged方法,如图
Material Design之CoordinatorLayout原理剖析在这个方法里面根据type,再去调用Behavior的相应方法。

  • 这也是把OnPreDrawListener作为监听者的原因。因为有些时候onMeasure或者onLayout可能不会调用,但是关于draw的方法是肯定会调用的。这样把onChildViewsChanged方法放在监听者的onPreDraw方法中,就可以在DepandedView发生变化的时候,及时调用onChildViewsChanged,通知其他依赖DepandedView的子View们发生相应的变化。当然这些变化的方法是由Behavior调用的

一个问题:mDependencySortedChildren是啥

ok,让我们思考一个问题。不知道大家发现没有,在onChildViewsChanged源码中,CoordinatorLayout得到子View的时候,不是调用的我们之前学过的getChildAt方法,而是调用的mDependencySortedChildren的相应方法。
Material Design之CoordinatorLayout原理剖析那么mDependencySortedChildren是个啥?我们点进去看看

Material Design之CoordinatorLayout原理剖析
我们看到了它下面有一个DirectedAcyclicGraph<View>类型的数据。

  • 我先解释下原因吧。在这里,因为CoordinatorLayout管理的不仅仅是子View,还有子View之间的关系(依赖关系)。也就是说不能用一个简单的集合,而是用这个数据结构,而DirectedAcyclicGraph就是一个图(具体来说是有向无环图,用邻接表来实现的),表示1对多的关系。当我们找到一个View,以及其与其他View之间的依赖关系时,就把这个View和相应依赖关系(图中叫做)存入这个DirectedAcyclicGraph中,然后再将其某种形式存入mDependencySortedChildren中。这就是mDependencySortedChildren

第二个问题,mDependencySortedChildren如何赋值的

mDependencySortedChildren的赋值和DirectedAcyclicGraph的赋值有很大关系,我们先看DirectedAcyclicGraph

onMeasure方法中

Material Design之CoordinatorLayout原理剖析

我们发现了prepareChildren,追踪进入

这里面就是具体赋值的流程,都写在注释中了

    private void prepareChildren() {
    	//--------------------------------------
        //--------------------------------------
        //首先对两者进行清空操作
        //--------------------------------------
        //--------------------------------------          
        mDependencySortedChildren.clear();
        mChildDag.clear();

        for (int i = 0, count = getChildCount(); i < count; i++) {
	        //--------------------------------------
	        //--------------------------------------
	        //然后得到子View,即view 
	        //--------------------------------------
	        //--------------------------------------   
            final View view = getChildAt(i);

            final LayoutParams lp = getResolvedLayoutParams(view);
            lp.findAnchorView(this, view);
			//--------------------------------------
	        //--------------------------------------
	        //把这个子view加入图中,作为图的节点
	        //--------------------------------------
	        //------------------------------------
            mChildDag.addNode(view);

            for (int j = 0; j < count; j++) {
                if (j == i) {
                    continue;
                }
                //--------------------------------------
		        //--------------------------------------
		        //然后再嵌套for循环,得到子View,即other 
		        //--------------------------------------
		        //------------------------------------
                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);
                    }
                    //--------------------------------------
			        //--------------------------------------
			        //如果view依赖other,或者说other是DepandedView时
			        //就画一条从other到view的边,并加入图中,表明view和other存在依赖关系
			        //--------------------------------------
			        //------------------------------------
                    mChildDag.addEdge(other, view);
                }
            }
        }

        //--------------------------------------
        //--------------------------------------
        //调用addAll方法,将图的getSortedList加入到mDependencySortedChildren中
        //--------------------------------------
        //------------------------------------
        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        //--------------------------------------
        //--------------------------------------
        //这里做了一个反转,无需理解,知道就行
        //--------------------------------------
        //------------------------------------
        Collections.reverse(mDependencySortedChildren);
    }

到这我们知道了CoordinatorLayout是通过图里面的边来区分谁依赖于谁的了

五.图示CoordinatorLayout下的事件传递机制

Material Design之CoordinatorLayout原理剖析

Material Design之CoordinatorLayout原理剖析

上一篇:基于MySQL的电商⽤户、商品、平台价值分析


下一篇:区块连技术基础