CoordinatorLayout探索

431 阅读2分钟

初次接触

项目案例:

要实现类似京东的首页顶部沉浸式状态栏

image.png

已有的方案:

项目中的实现原理状态栏透明+fitsSystemWindows+paddingTop image.png

image.png

FrameLayout对于android:fitsSystemWindows属性是没有进行处理的,所以FrameLayout作为顶部父布局时,内部的布局不管设不设置都不会产生什么变化。

FrameLayout+SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN:

image.png

image.png

CoordinatorLayout:

image.png image.png

现在的方案:

CoordinatorLayout+CollapsingToolbarLayout

CollapsingToolbarLayout一定要结合着CoordinatorLayout一起使用,而不能单独使用。因为CollapsingToolbarLayout只会对内部控件的偏移距离做出调整,而不会像CoordinatorLayout那样调用setSystemUiVisibility()函数来开启沉浸式状态栏。

image.png

image.png

image.png

CoordinatorLayout(协调器布局)

image.png

第一个用例:

作为顶层应用程序或者chrome布局 有一些特殊的api,比如设置状态栏的颜色和背景 作为NestedScrollingParent使用在滑动的场景

image.png

第二个用例(重点):

关键在于给子视图添加Behaviors可以实现交互

image.png

image.png

一般我们自定义一个 Behavior,目的有两个:

  • 一个是根据某些依赖的 View 的位置进行相应的操作
  • 另外一个就是响应 CoordinatorLayout 中某些组件的滑动事件
两个 View 之间的依赖关系

如果一个 View 依赖于另外一个 View。那么它可能需要操作下面 3 个 API:

image.png

image.png

image.png

  • 确定一个 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一起做相应的位移了。

image.png

使用behavior

image.png

image.png

image.png

源码解析
# 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结合

image.png

image.png

通过 behavior 的这些方法,基本上我们不需要通过去重写一个 View,就可以通过 behavior 来扩展和增强它的交互功能(源码里也提供了一些通用的Behavior)

insetEdge&dodgeInsetEdges

insetEdge来设置插入CoordinatorLayout的方向,dodgeInsetEdges设置的是所避让的方向,可以是一个或多个方向,也可以设置为all,即避让所有方向。

(eg:Snackbar)FloatingActionButton自带app:layout_dodgeInsetEdges="bottom",而Snackbar自带app:layout_insetEdge="bottom",所以当Snackbar出现的时候FloatingActionButton能发生躲避行为

image.png

image.png

// 代码案例

image.png

// 原理解析

onChildViewsChanged(@DispatchChangeEvent final int type) 方法。

在这个方法,使用了一个变量 Rect inset,在遍历时记录当前遍历过的 View 在各种方向上所覆盖的最大距离。同时,每个子 View 也会检查自己设置的躲避的方向上,与这一条边的距离是否小于 inset 上所记录的距离,如果小于,则说明自己会被覆盖,那么就进行位移。

image.png

image.png

image.png

假设父布局 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 的测量尺寸、布局位置、触摸响应。

image.png

Android应用内常用的场景:

image.png

image.png

结合MotionLayout的示例:developer.android.google.cn/training/co…

一个综合的案例:juejin.cn/post/697644…

参考:

developer.android.google.cn/reference/a…

blog.csdn.net/qq_33209777…

mp.weixin.qq.com/s?__biz=MzA…

blog.csdn.net/briblue/art…

blog.csdn.net/maosidiaoxi…