CoordinatorLayout原理解析与Behavior
CoordinatorLayout的功能
事先警示:CoordinatorLayout只继承了NestedScrollParent,也就是说它只能做顶层父View,别踩坑!!!
处理子控件之间依赖下的交互
处理子控件之间的嵌套滑动
处理子控件的测量与布局
处理子控件的事件拦截与响应
以上四个功能,都建立与CoordinatorLayout中提供的一个叫做Behavior的“插件”之上。Behavior内部也提供了相应方法来对应这四个不同的功能。
Behavior内部集成了上述四种功能对应的方法,实现解耦。
什么是Behavior?
可以把Behavior理解成插件,当我们的组件想实现什么样的功能的时候就使用对应的Behavior,也就是说很多种不同功能的Behavior,当然也可以自定义Behavior。
CoordinatorLayout下依赖交互原理
当CoordinatorLayout中子控件dependency的位置、大小发生变化的时候,那么在CoordinatorLayout内部会通知所有依赖depandency的控件,并调用对应声明的Behavior,告知其依赖的dependency发生变化。
那么如何判断依赖->layoutDependsOn
接收到通知后如何处理->onDependentViewChanged/onDependentViewRemoved
这些都是由Behavior来处理。
主要方法介绍
layoutDependsOn(CoordinatorLayout parent, View child, View dependency) 表示是否给应用了Behavior 的View 指定一个依赖的布局
- 参数1:coordinatorlayout对象
- 参数2:child 被观察的View
- 参数3:依赖变化的View(被观察的View)
onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) 当依赖的View发生变化的时候hi掉的方法
onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) 当用户手指按下的时候,你是否要处理这次操作。当你确定要处理这次操作的时候,返回true;如果返回false的时候,就不会去响应后面的回调事件了。你想怎么滑就怎么话,我都不做处理。这里的(axes)滚动方向很重要,可以通过此参数判断滚动方向!
- 参数3:直接目标,相当于能滑动的控件
- 参数4:观察的View
- 参数5:这个可以简单理解为滚动方向
- ViewCompat#SCROLL_AXIS_HORIZONTAL 水平方向
- ViewCompat#SCROLL_AXIS_VERTICAL 竖直方向
- 参数6:这个参数是之后有的,如果你输入的类型不是TYPE_TOUCH那么就不会相应这个滚动
onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type) 当onStartNestedScroll准备处理这次滑动的时候(返回true的时候),回调这个方法。可以在这个方法中做一些响应的准备工作!
onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type) 当滚动开始执行的时候回调这个方法。
- 参数4/参数5:用户x/y轴滚动的距离(注意这里是每一次都回调的啊!!!)
- 参数6:处理滚动的距离的参数,内部维护着输出距离,假设用户滑动了100px,child 做了90px的位移,你需要把consumed[1]的值改成90,这样coordinatorLayout就能知道只处理剩下的10px的滚动。其中consumed[0]代表x轴、consumed[1]代表y轴。可能你不理解这个问题,换个形象点的比喻,比如你开发某一个功能,但是你只会其中的90%那么怎么办呢?不能就不管了。好你找到了你的同事或者老大,让他去完成剩下的10%。这样问题就完美的解决了,是一个概念的!
onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) 上面这个方法结束的时候,coordinatorLayout处理剩下的距离,比如还剩10px。但是coordinatorLayout发现滚动2px的时候就已经到头了。那么结束其滚动,调用该方法,并将coordinatorLayout处理剩下的像素数作为参
(dxUnconsumed、dyUnconsumed)
传过来,这里传过来的就是 8px。参数中还会有coordinatorLayout处理过的像素数(dxConsumed、dyConsumed)。老大开始处理剩下的距离了!这个方法主要处理一些越界后的滚动。还是不懂对吧!还拿你们老大做比喻:比如上面还剩 10%的工作,这时老大处理了2%后发现已经可以上线了,于是老大结束了工作,并将处理剩下的内容(dxUnconsumed、dyUnconsumed)纪录下来,告诉你。老大处理了的内容(dxConsumed、dyConsumed)也告诉了你。- 参数4/参数5:当没有滚动到顶部或者底部的时候,x/y轴的滚动距离
- 参数6/参数7:当滚动到顶部或者底部的时候,x/y轴的滚动距离
onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY) 当手指松开发生惯性动作之前调用,这里提供了响应的速度,你可以根据速度判断是否需要进行折叠等一系列的操作,你要确定响应这个方法的话,返回true。
- 参数4/参数5:代表相应的速度
onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int type) 停止滚动的时候回调的方法。当你不去响应Fling的时候会直接回调这个方法。在这里可以做一些清理工作。或者其他的内容。。。
onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) 确定子View位置的方法,这个方法可以重新定义子View的位置(这里明确是设置behavior的那个View哦),例如下面这样
- ViewCompat#LAYOUT_DIRECTION_LTR 视图方向从左到右
- ViewCompat#LAYOUT_DIRECTION_RTL 视图方向从优到左
CoordinatorLayout下嵌套滑动原理
CoordinatorLayout实现了NestedScrollingParent2接口。那么当事件(scroll或fling)产生后,内部实现了NestedScrollingChild接口的子控件会将事件分发给CoordinatorLayout,CoordinatorLayout又会将事件传递给所有的Behavior中实现子控件的嵌套滑动。
相对于NestedScrollView机制(参数角色只有子控件和父控件),CoordinatorLayout中的交互角色玩出了新高度,在CoordinatorLayout下的子控件可以与多个兄弟控件进行交互。
大家可以看一下整个事件的流程图
浅谈View的生命周期
层级关系:activity->window->view
LayoutInflater.inflate将view解析出来
在onFinishInflate阶段add到content中,content属于DecorView
在Activity中通过attach方法new一个PhoneWindow
在resume阶段通过makevisible方法PhoneWindow.addView(DecorView)
再回调onAttachToWindow,然后已经一系列方法(包括setView->WMS…开始绘制)才呈现出View
源码实现
下面是截取的重要源码截图
ViewTreeObserver介绍
ViewTreeObserver注册一个观察者来监听视图树,当是图书的布局、视图树的焦点、视图将要绘制、视图滚动等发生改变时,ViewTreeObserver都会收到通知,ViewTreeObsrver不能被实例化,可以调用,View.getViewTreeObserver()来获得
dispatchOnOreDraw():通知观察者绘制即将开始,如果其中的某个观察者返回true,那么绘制会取消,并且重新安排绘制,如果想在View Layout或View hierarchy还未依附到Window时,或者在View处于CONE状态时强制绘制,可以手动调用这个方法。
通过ViewTreeObserver添加绘制监听
注意,这里不是getChildAt()
而是mDependencySortedChildren.get()
,因为mDependencySortedChildren在这里是一个特殊的集合,因为CoordinatorLayout管理着多个子View,并且子View之间还存在依赖关系,所以还利用了有向无环图来记录View的依赖关系。只需理解原因即可,这里不用深究,否则会陷到坑里面去。
Behavior在哪里被实例化?
如果自定义ViewGroup并且需要提供一些特殊的属性给子View,那么就需要重写LayoutParams
HierarchyChangeListener
回调监听,当childView添加与删除时调用
前面提到了mDependencySortedChildren,那么mDependencySortedChildren在哪初始化呢?
Demo
效果展示
这是利用Behavior实现的嵌套滑动效果,上滑时顶部会隐藏,往下滑时顶部又会回显出来
public class SampleHeaderBehavior extends CoordinatorLayout.Behavior<TextView> { |
上面demo其实是对前面提到的嵌套滑动方法的实践。而这个例子才是对依赖与被依赖组件关系的效果展示,红色方块是被依赖方,而“跟随兄弟”和变色兄弟都是依赖方,会根据红色方块的位置变化而变化。
public class DependedView extends View { |
总结
用好Behavior对我们做好看的UI效果有很大的帮助,毕竟NestedScrollParent和NestedScrollChild也只能实现两个组件之间的嵌套关系,而Behavior能够实现一对多的嵌套关系。这节内容主要是对CoordinatorLayout的介绍以及只是对自定义Behavior的浅尝试,后面小编将写一篇真正意义的自定义Behavior的Demo介绍与实现。