破题
从CoordinatorLayout的文档中可以了解到,其有两个使用场景,其一是作为顶层应用程序布局装饰,这个用例我们不谈;其二是作为协调一个或多个子View间交互的的容器。那么我们可以用另外一种表达去阐述问题:为了达到方便协调子View的目的,它采用了哪些设计?
我们可能的思路
抛开其他的不谈,我们可以先思考如何用自己的思路去实现这一功能。协调子View意味着什么?意味着某一子view的行为会影响另外子View的行为,那么这就需要一个中枢系统去协调,去传递事件。这就像设计模式中的中的中介模式--用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
一般来说,对于View行为的拓展是通过继承的方式来实现的。依循这种思路,我们发现要实现CoordinatorLayout的功能是不可能。1:我们必须通过继承现有的View类,去扩展View的能力--至少它需要能接受兄弟View行为变化通知的能力;2:如果我们不扩展子View,那么我们就必须把CoordinatorLayout做的很强大,它去感知子View的行为,同时它又去改变另一个子View的行为。那么CoordinatorLayout就会变得臃肿且很难扩展。
分析至此,我们先明确设计目标 :
1: 不去改变现有的的View组件,以及View System 。
2:能够容易的扩展View组件的能力。
Android团队的实现
那么google android 团队是如何实现CoordinatorLayout的呢?
其核心设计是引入Behavior。Behavior翻译过来就是行为。我们知道View的功能是非常复杂的,主要有两个方面:1:去测量,布局,绘制自己,2:接收触摸事件去做出反应。我们扩展View的功能一般都是从这两个方面去扩展。Behavior的要点就是将View的这两方面行为单独抽象出来得到一个接口,我们可以通过扩展这个接口,从而扩展View的行为。我们也可以通过Behavior这个接口中的方法直观感受到这点--onMeasureChild和onLayoutChild方法是为了扩展子View的第一个方面onInterceptTouchEvent和onTouchEvent是为了扩展第二个方面。我们很容易发现,这两个扩展点都是为了扩展View本身,它将本应该用继承的方式实现的扩展,通过Behavior这个代理去实现。换句话说,就是Behavior的这两方面能力并未切中我们所讨论问题的核心--Behavior如何协调CoordinatorLayout的子View的?
回到这个核心问题的讨论上来。通过以上分析可知,Behavior必须为View添加另外一种能力--就是接受CoordinatorLayout的协调通知,并对此作出反应,而这通知的内容就是兄弟View的行为变化。CoordinatorLayout的子View能够感知兄弟View的这种行为,design包的开发团队把其称为依赖(dependency)--我能感受到你的行为变化,就表示我依赖你。兄弟View之间存在一种依赖关系,CoordinatorLayout则维护这种依赖关系。
在此,我们可以提出以下问题:
1:兄弟View如何建立依赖关系?
2:CoordinatorLayout如何维护子View间的依赖关系?
先回答问题1.通常依赖关系的主体有两个,一个是被依赖者,另一个是依赖者。前者报告自己的行为状态的变化,后者接收这种变化,做出反应。
被依赖者
我们先看Behavior如何为View赋予被依赖者的能力的。Behavior接口中有如下方法:onStartNestedScroll,onNestedScrollAccepted,onStopNestedScroll,onNestedScroll,onNestedPreScroll,onNestedFling。我们发现v21后的android sdk中的View类也增加了相应的同名方法,也就是说Android团队为Android View System增加了新的能力。这是怎样一种能力呢?
我们知道onInterceptTouchEvent和onTouchEvent是视图层级触摸事件分发机制的核心。这种机制简单的可以概括为:触摸事件从父视图流向子视图,在流动过程中,父视图可以观察事件,并决定是否拦截,一旦父视图决定拦截,将不会将事件在传递给子视图。这种流动是单向的,并且有单一终点--最终消耗事件的View。这种事件分发机制显然不满足我们的要求。最直观的一点就是,但我们把手放在视图A滚动的时候,视图B也会联动,也就是说,同样的触摸事件被两个甚至多个视图使用了。当然,我们也能通过其他实现这种功能。只是我们需要的是一种具有普适性的解决方法,而不是每次需要这种功能就去写一遍同样逻辑的代码。因此Android团队把这种能力添加进了View System的最基本类--View类--中 。 Android团队仍保留了之前的分发机制,只是在最后一个环节上做出了修改。我们这里说的最后一个环节是指,触摸事件到达了目标视图的onTouchEvent方法。之前的实现很简单就是被目标视图直接消耗掉,就一个步骤。而现在则变成了三个步骤:
1:目标视图先把事件回送到关心此事件的父视图,让其作出处理;
2:目标视图根据父视图的处理情况,调整自己的处理策略;
3:目标视图再次将自己消耗后的触摸事件传递给父视图,父视图根据需要作出处理。
我们可以形象一点形容以前的分发机制是一条直线,从父视图到子视图,而现在的机制就是一条有任意闭环的曲线,事件到达了子视图后能回流到父视图。有了这套机制解决我们之前的问题就很简单了--触摸事件到达了视图A,视图A在处理之前先交给关心这个事件的父视图,父视图则完全有能力把触摸事件发送给另外一个子视图B,传递的过程中有一些附带的信息可供父视图,A视图,B视图使用,利用这些信息整个视图层级的联动过程可以协调一致。
上文我提到父视图时,都会用“关心这个事件的”定语去修饰。在实现上,父视图可以通过实现NestedScrollingParent接口,表明自己关心子视图回传的触摸事件。我们之前提到的onStartNestedScroll 等方法皆存在与这个接口中。
解释到这里,我们似乎有点忘记了我们所要解决的问题了。在此,我重新说明一下:Behavior具有NestedScrollingParent接口中的同名方法,但是不是通过直接实现NestedScrollingParent接口得到的,这意味什么?而CoordinatorLayout则实现了NestedScrollingParent接口,这与前一个问题有什么联系?我简单回答一下,CoordinatorLayout就是能接收子视图回传事件的父视图,它将收到的事件又原封不动的转发给另外一个子视图。我们现在终于可以回答最初的问题了--Behavior如何为View赋予被依赖者的能力的。答案很简单!因为它能获得第一手的触摸事件啊。它能在第一时间反应,改变自己的位置或者大小,那么整个视图层级为了达到协调,必然会依赖它--通过它的位置调整自己的位置。
依赖者
那么Behavior如何为View赋予依赖者的能力的呢?回答这个问题就很简单了,依赖者只需要告诉CoordinatorLayout它依赖哪个兄弟视图,每当它所依赖的兄弟视图行为改变的时候,CoordinatorLayout都会通知依赖者,此时依赖者就能做出相应改变了。在实现上Behavior添加了layoutDependsOn,onDependentViewChanged,onDependentViewRemoved,让View具备依赖者的能力。
我们现在总结一下,Behavior具有以下四个方面的能力:
1:修改View测量,布局的能力(onMeasureChild等方法);
2:拦截或消耗触摸事件的能力(onTouchEvent等方法);
3:赋予View被依赖者的能力(NestedScrollingParent接口同名方法);
4:赋予View依赖者的能力(layoutDependsOn等);
1,2两点是对View的自身的扩展,3,4是为了配合CoordinatorLayout的行为作出的扩展。
维护依赖关系
现在解决第二个问题--CoordinatorLayout如何维护子View间的依赖关系?
这种依赖关系,源码是用无环有向图这种数据结构表示的 ,也就是说是单向的,不能循环依赖的。前文我已经基本上阐明了CoordinatorLayout的工作机制--子视图间如何通过所建立的依赖的关系彼此协调,所以对于第二个问题,也没有必要过多解释。
还有一个重要的小问题--Behavior是如何整合到Android View System中的呢?
看源码很容易找到答案, CoordinatorLayout的每一个子View都与一个CoordinatorLayout.LayoutParams相关,当然,这是View System本来就有的设计,而Behavior则是CoordinatorLayout.LayoutParams类的成员变量,这样一来,CoordinatorLayout的每一个子View都会与一个Behavior相关。就这样Behavior无缝的整合到了View System中。最让人拍案叫绝的是,我们自定义了一个Behavoir,我们可以把它附着到任何控件类中。想想我们以前怎么做--用继承的方式实现。Behavior是赋予控件新行为最一劳永逸的方法。一次编写,任何一个控件都能使用。
最后的话
在构思这篇文章的时候,我就决定要写一篇只讲思路,不贴代码的文章。这必然会使得这篇文章对于没看过源码的读者有些不容易理解。所以,我建议读者结合源码来读。
网友评论