前言
这个系列源自前几天看到一篇使用CoordinatorLayout实现支付宝首页效果的文章,下载看了效果和源码,不敢苟同,所以打算自己动手。实现的过程有点曲折,但也发现了一些有意思的事情,用三篇文章来记录并分享给大家。
- CoordinatorLayout和Behavior
- 自定义CoordinatorLayout.Behavior
- 支付宝首页效果实现
文中:CoL代表CoordinatorLayout,ABL表示AppBarLayout,CTL表示CollapsingToolbarLayout,SRL表示SwipeRefreshLayout,RV表示RecyclerView。
源码:Github
第一篇文章主要讨论Behavior的结构、CoordinatorLayout的实现以及CoordinatorLayout和Behavior之间的通信。除了Behavior相关的内容,CoordinatorLayout作为官方实现的一个ViewGroup,也有一些在自定义ViewGroup时可以借鉴的内容,这些也穿插在这篇文章中。
Behavior结构
使用CoordinatorLayout结合ABL,CTL,SRL/RV可以方便的实现各种MaterialDesign ToolBar效果,还有FloatingActionButton,SnackBar等控件,可以直接使用。CoL的主要功能是为其直接子View提供 协调滚动 的统一接口,让子View可以方便的实现诸如嵌套滚动,跟随滚动等效果,让界面更加灵动;而这个统一接口,就是Behavior。
我们先来看一下Behavior提供的方法,大致可以分为4组:
布局相关,这类方法用来重载child的Measure、Layout相关回调。
1 | public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed); |
Touch事件相关,这组方法用来拦截和处理Touch事件传递。
1 | public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev); |
NestedScrolling相关,这组方法用来响应NestedScrolling,更多关于NestedScrolling的讨论可以查看另一篇博客。
1 | public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes); |
其他辅助方法
1 | // 关联/取消关联LayoutParams的时候回调 |
多数情况下我们只需要关注前三组方法。通过这些方法我们可以看到Behavior可以做的事情不仅仅是“依赖某个View的变化并且在其变化之后进行响应”这么单一,Behavior实际上可以控制child在CoL中的measure,layout以及拦截toush事件,支持NestedScrolling等等,基本上是除了Draw之外的全部自定义View需要关注的内容了。
Behavior的使用和自定义我们下一篇文章进行讨论,这篇文章我们继续关注CoL如何操作Behavior。
设置Behavior
给一个View设置Behavior有两种方法:在布局文件中指定;使用@CoordinatorLayout.DefaultBehavior
注解。
第一种方法适用于多数情况,需要注意的是,如果使用自定义Behavior,需要覆写2个参数的构造方法:
1 | public Behavior(Context context, AttributeSet attrs); |
因为CoL在解析时是通过反射调用Behavior的这个构造方法创建Behavior对象的:
1 | static Behavior parseBehavior(Context context, AttributeSet attrs, String name) { |
第二种方法,适用于自定义View并且自定义Behavior的情况,比如AppBarLayout:
1 | .class) .DefaultBehavior(AppBarLayout.Behavior |
如果在布局文件中不另外指定,这里将调用Behavior的无参构造方法创建对象。
事件分发
上面看到的Behavior的各个方法,其调用者基本都是CoL。CoL在自己的回调方法中通过调用子View Behavior的相关方法,将事件向下分发。
在讨论分发方法之前,有一点需要注意:Behavior虽然影响的是子View的布局和行为,但实际是对CoL本身事件处理的代理。
基本的模式大家可以想到,就是遍历子View,获取Behavior,然后调用子Behavior对应的方法。这里对几个有意思的地方进行讨论:
Touch事件分发
Touch事件分发分两个方法onInterceptTouchEvent和onTouchEvent。CoL的实现中,这两个方法都调用performIntercept
方法将是否拦截事件的判断交给Behavior处理。每个CoordinatorLayout的子View都有机会拦截事件并响应,注意这里子View并不是在自己的onTouch相关方法中进行处理,而是Behavior子类,有机会代理CoL对事件进行拦截并处理。
出于篇幅考虑这里不贴源码了,关键的地方这里解释一下:
- 在遍历子View之前,使用
getTopSortedChildren(topmostChildList);
获取按照显示顺序由上至下排序过的子View列表。ViewGroup可以覆写getChildDrawingOrder
自定义子View的绘制顺序(这篇文章中有对这个方法的解释),getTopSortedChildren方法会按照绘制顺序获取子View;在5.0及以上版本中,还要考虑Z轴次序,也就是elevation,会再进行一次排序,最终得到真实可靠的自顶之下的子View分发顺序。这对让子View合理响应Touch事件很重要,如果自定义Group需要有类似功能,可以参考CoL的实现。 - Behavior通过覆写
onInterceptTouchEvent
或者onTouchEvent
并返回true来声明拦截事件,CoL会将该View缓存到mBehaviorTouchView属性,后续事件将直接分发到该View。直到该View的onTouchEvent方法返回false。 - 在确定mBehaviorTouchView之后,CoL会将该View(Z轴)下面View的事件流终止,具体操作是向这些View分发一个CANCEL事件。
Behavior可以通过覆写blocksInteractionBelow方法block下方View的事件。在自己不需要处理事件但同时不希望子View处理事件时,可以简单的覆写这个方法。
默认实现逻辑是判断getScrimOpacity的值>0
NestedScrolling
NestedScrolling是Behavior实现滑动的重要支撑。前文提到Behavior是对CoL自身事件的代理,所以Behavior对NestedScrolling的支持,就是在代理CoL的NestedScrollingParent接口方法。
更多NestedScrolling相关信息参见更早的博客:Android Nested Scrolling
需要注意的是关于NestedScrolling机制中“消费量”的处理
1 |
|
注意这里是取了所有Behavior消费掉偏移量的最大值。因为Behavior是代理的角色,而各个代理的消费对于NestedScrolling机制来说,都会被看做是CoL这个NestedScrollingParent的消费。各Behavior之间是同级的,所以他们对事件的消费是“重叠”的(可以重复消费),所以这里返回的consumed是取最大值。
LayoutDependence
这里是其他博客讲的比较多的地方,确定View依赖的Dependence,当Dependence变化之后,会将变化广播给所有依赖这个View的兄弟View。我要说的有两点:1. onDependentViewChanged回调的调用时机。2. 依赖关系的存储。
- onDependentViewChanged在某个View的大小或者位置发生变化的时候都会进行回调。并且是真正变化之后才会进行回调。
- 子View之间的依赖关系通过非循环有向图数据结构进行存储。具体到结构上就是通过一个
Map<Node, List<Node>>
存储(这个Map并不是JDK的实现,感兴趣的可以看下源码)。 - 既然存在依赖关系,那么在涉及到对子View遍历的时候,就要考虑到子View之前的依赖关系。CoL的实现中
prepareChildren
方法构建依赖图并根据依赖图进行DFS搜索得到依赖链列表,这个列表用在了分发布局、NestedScrolling、LayoutDependentChange的过程中。
自定义LayoutParams
自定义ViewGroup很常见,但是多数情况下用不到自定义LayoutParams。LayoutParams正如其名,用了设置布局参数,也就是控制ViewGroup如何measure和layout子View。如果一个自定义ViewGroup提供了额外的布局参数,那就需要自定义LayoutParams了。自定义LayoutParams并没有多么复杂,这里列几个需要注意的地方。
基类
如果自定义LayoutParams需要支持margin,继承自ViewGroup.MarginLayoutParams
即可,默认的ViewGroup.LayoutParams并不支持margin。
构造方法
LayoutParams默认有多个不同参数的构造方法:
- LayoutParams(Context c, AttributeSet attrs) 适用于解析布局文件时生成LayoutParams,layout相关的xml属性,就是在这个构造方法里面解析的。
- LayoutParams(int width, int height) 代码构建时只传入宽高。
- LayoutParams(LayoutParams source) LayoutParams转换
- LayoutParams() 无参构造函数
自定义LayoutParams也要覆写这些构造方法并做响应的转换。
ViewGroup方法
使用自定义LayoutParams的ViewGroup,也需要实现几个相关方法,主要是在解析、addView的时候生成适合的LayoutParams。
1 | protected LayoutParams generateDefaultLayoutParams(); |
具体实现可以参考CoL。
CoordinatorLayout.LayoutParams
CoL.LP主要实现了基于anchorView的布局和keylines(纵向基准线,子View可以对齐到keyline)的布局、保存Behavior、储存滑动过程中的标记位,具体实现这里就不展开了,逻辑比较简单。