初次接触
项目案例:
要实现类似京东的首页顶部沉浸式状态栏
已有的方案:
项目中的实现原理状态栏透明+fitsSystemWindows+paddingTop
FrameLayout对于android:fitsSystemWindows属性是没有进行处理的,所以FrameLayout作为顶部父布局时,内部的布局不管设不设置都不会产生什么变化。
FrameLayout+SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN:
CoordinatorLayout:
现在的方案:
CoordinatorLayout+CollapsingToolbarLayout
CollapsingToolbarLayout一定要结合着CoordinatorLayout一起使用,而不能单独使用。因为CollapsingToolbarLayout只会对内部控件的偏移距离做出调整,而不会像CoordinatorLayout那样调用setSystemUiVisibility()函数来开启沉浸式状态栏。
CoordinatorLayout(协调器布局)
第一个用例:
作为顶层应用程序或者chrome布局 有一些特殊的api,比如设置状态栏的颜色和背景 作为NestedScrollingParent使用在滑动的场景
第二个用例(重点):
关键在于给子视图添加Behaviors可以实现交互
一般我们自定义一个 Behavior,目的有两个:
- 一个是根据某些依赖的 View 的位置进行相应的操作
- 另外一个就是响应 CoordinatorLayout 中某些组件的滑动事件
两个 View 之间的依赖关系
如果一个 View 依赖于另外一个 View。那么它可能需要操作下面 3 个 API:
- 确定一个 View 对另外一个 View 是否依赖的时候,一种就是直接通过 xml 锚定一个 View,另一种就是通过 layoutDependsOn() 这个方法。注意参数,child 是要判断的主角,而 dependency 是宾角,如果 return true,表示依赖成立,反之不成立。只有在 layoutDependsOn() 返回为 true 时,后面的 onDependentViewChanged() 和 onDependentViewRemoved() 才会被调用。
- 当依赖的那个 View 发生变化时,也就是 dependency 的尺寸和位置发生的变化,当有变化时 Behavior 的 onDependentViewChanged() 方法会被调用。
- onDependentView() 被调用时一般是指 dependency 被它的 parent 移除,或者是 child 设定了新的 anchor。
使用anchor:
child B通过layout_anchor 设置child A为anchor,再通过layout_anchorGravity来根据需要设置属性,这样B就可以和A一起做相应的位移了。
使用behavior
源码解析
# CoordinatorLayout.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
prepareChildren();
ensurePreDrawListener();
...
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final Behavior b = lp.getBehavior();
// 会在这里执行 Behavior#onMeasureChild() 方法 将onMeasure() 传递给childView
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
}
}
# prepareChildren:
// 保存子view
private final List<View> mDependencySortedChildren = new ArrayList<>();
// 有向无环图的数据结构
private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();
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);
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)) {
mChildDag.addNode(other);
}
mChildDag.addEdge(other, view);
}
}
}
// 添加到 mDependencySortedChildren 中
mDependencySortedChildren.addAll(mChildDag.getSortedList());
Collections.reverse(mDependencySortedChildren);
}
# ensurePreDrawListener:
void ensurePreDrawListener() {
...
if (hasDependencies != mNeedsPreDrawListener) {
if (hasDependencies) {
// 会走这里
addPreDrawListener();
} else {
removePreDrawListener();
}
}
}
void addPreDrawListener() {
// mIsAttachedToWindow 在 onAttchedToWindow 设置为 true
if (mIsAttachedToWindow) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
mNeedsPreDrawListener = true;
}
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
// 如果ChildView有任何变化 就会执行这里
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
// 通过双层for循环,找到依赖的view
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) {
continue;
}
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// 当view删除的时候调用这里
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// 如果coordinatorLayout的childView发生一点点变化,就会执行到这里
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
}
}
}
总结:
其实Behavior的原理就是观察者模式的应用,被观察者就是事件源dependency,观察者就是做出改变的child。
在onMeasure()的时候保存childView,通过 PreDrawListener监听childView的变化,最终通过双层for循环找到对应的Behavior,分发任务即可。
Behavior 对滑动事件的响应
因为CoordinatorLayout实现了NestedScrollingParent,所以可以配合NestedScrollingChild使用实现嵌套滑动
嵌套滑动分为 nested scroll 和 fling 两种。Behavior 中相应的 View 是否接受响应由 onStartNestedScroll() 返回值决定。一般在 onNestedPreScroll() 处理相应的 nested scroll 响应,在 onPreFling 处理 fling 事件。
// 代码案例 ImageView与NestedScrollView结合
通过 behavior 的这些方法,基本上我们不需要通过去重写一个 View,就可以通过 behavior 来扩展和增强它的交互功能(源码里也提供了一些通用的Behavior)
insetEdge&dodgeInsetEdges
insetEdge来设置插入CoordinatorLayout的方向,dodgeInsetEdges设置的是所避让的方向,可以是一个或多个方向,也可以设置为all,即避让所有方向。
(eg:Snackbar)FloatingActionButton自带app:layout_dodgeInsetEdges="bottom",而Snackbar自带app:layout_insetEdge="bottom",所以当Snackbar出现的时候FloatingActionButton能发生躲避行为
// 代码案例
// 原理解析
onChildViewsChanged(@DispatchChangeEvent final int type) 方法。
在这个方法,使用了一个变量 Rect inset,在遍历时记录当前遍历过的 View 在各种方向上所覆盖的最大距离。同时,每个子 View 也会检查自己设置的躲避的方向上,与这一条边的距离是否小于 inset 上所记录的距离,如果小于,则说明自己会被覆盖,那么就进行位移。
假设父布局 CoordinatorLayout 的坐标点为0, 0, 320, 480,View A 设置了 insetEdges 的方向是所有方向,并且它的坐标点是[50, 60, 150, 180],那么底边与顶部边缘的距离就是 180,顶边与底部边缘的距离就是 420,以此类推,得到 inset的值为[150, 180, 270, 420]。
再定 View B 只设定了 insetEdges 的方向为Top,它的坐标点是[100, 220, 200, 300],那么遍历到 View B 的时候就会计算出 inset的值为[150, 300, 270, 420]。
如果 View C 的坐标点是[150, 270, 300, 440],并且只设定了dodgeInsetEdges(躲避内嵌边缘)方向为Top,由于它的顶部到父布局顶边的距离是270,小于 inset.top 的 300 这个值,也就是顶部会被遮挡,那么就调用 setInsetOffsetY(View child, int offsetY) 方法对它进行位移,让它避免被遮挡,同时记录下所位移的值,从而实现 dodgeInsetEdges 的特性。
总结
Behavior 是一种插件机制,如果没有 Behavior 的存在,CoordinatorLayout 和普通的 FrameLayout 无异。Behavior 的存在,可以决定 CoordinatorLayout 中对应的 childview 的测量尺寸、布局位置、触摸响应。
Android应用内常用的场景:
结合MotionLayout的示例:developer.android.google.cn/training/co…
一个综合的案例:juejin.cn/post/697644…