Android CoordinatorLayout和Behavior解析
目录
在Materials Design中有一个名为CoordinatorLayout
的布局,这是一个神奇的布局,可以实现各种控件间的联动效果,比如底部FloatingActionBar
跟随Snackbar
弹出而上移
比如AppBarLayout
跟随NestedScrollView
滑动而伸缩,FloatingActionBar
跟随AppBarLayout
伸缩而显隐
这些都是非常赞的效果实现,这次我们就从源码角度来分析下这个布局和协助它实现控件联动效果的Behavior
.
CoordinatorLayout
特性
要知道一个类的特性,应当从类继承和接口开始
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
//...............
}
从上面可以知道这个布局是一个ViewGroup
,而且支持作为嵌套滑动的父布局.
对于一个ViewGroup
,应该关心什么呢?
个人觉得比较重要的有这几点
- 测量过程
- 布局过程
- 绘制过程
- 触摸事件处理
接下来看看CoordinatorLayout
的这些重点过程的处理方式
CoordinatorLayout
的测量过程
先查看其测量过程,其onMeasure
方法的核心代码如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//...............
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
//..................
final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
lp.leftMargin + lp.rightMargin);
heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin);
childState = View.combineMeasuredStates(childState, child.getMeasuredState());
}
final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
childState & View.MEASURED_STATE_MASK);
final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
childState << View.MEASURED_HEIGHT_STATE_SHIFT);
setMeasuredDimension(width, height);
}
代码主体是测量每一个子View的宽高,然后取子View中最大的距离消耗作为自己的宽高,这种方式貌似和FrameLayout
很像.
然后有一段值得注意的代码
final Behavior b = lp.getBehavior();
if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0)) {
onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
childHeightMeasureSpec, 0);
}
这里子View的测量过程居然可以使用子View的Behavior
的onMeasureChild
方法代替,这感觉就像被黑客劫持了一样,子View自带的测量都废了.
CoordinatorLayout
的布局过程
再看其布局过程,查看其onLayout
代码如下
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
if (child.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior behavior = lp.getBehavior();
if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
onLayoutChild(child, layoutDirection);
}
}
}
这里也是一样如果有Behavior
存在,则使用Behavior
中的布局方法.
如果没有Behavior
呢?
继续追踪CoordinatorLayout
自带的onLayoutChild
方法
public void onLayoutChild(View child, int layoutDirection) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.checkAnchorChanged()) {
throw new IllegalStateException("An anchor may not be changed after CoordinatorLayout"
+ " measurement begins before layout is complete.");
}
if (lp.mAnchorView != null) {
layoutChildWithAnchor(child, lp.mAnchorView, layoutDirection);
} else if (lp.keyline >= 0) {
layoutChildWithKeyline(child, lp.keyline, layoutDirection);
} else {
layoutChild(child, layoutDirection);
}
}
由于其后续涉及的代码较多,在此只做简单说明
如果子View的LayoutParams
设置了作为锚点的View(mAnchorView
),那么会获得锚点View的Rect
坐标,然后再借助子View的LayoutParams
中Gravity
设置坐标;
如果子View没有设置锚点View,但是设置了keyline(这个只是CoordinatorLayout
的keylines的index),且需要CoordinatorLayout
也设置了keylins数组,然后使用keyline结合Gravity
设置坐标,其中的CoordinatorLayout
中的keylines是以dp为单位的一组int数组,用于限制子View横坐标,作用不大而且非本篇重点,就此略过;
如果什么都没有设置则是只根据Gravity
布局,这点和FrameLayout
也是一致的.
在onLayout
中的布局是根据一个子View列表mDependencySortedChildren
依次布局的,查看这个子View列表的定义
private final List<View> mDependencySortedChildren = new ArrayList<>();
看名字都知道,这是特殊排序过的,这个列表就很有意思了.
由于子View的Behavior
可能对其它子View可能存在位置依赖关系,为了实现将被依赖的子View先布局而创建了这个列表.这个列表如何排序生成的呢?源码中在CoordinatorLayout
的onMeasure
中的prepareChildren
中生成一个无回路有向图(DirectedAcyclicGraph
),然后使用深度优先遍历算法(DFS
)将图遍历出来,再进行反序处理(Collections.reverse
)生成的,对算法比较感兴趣的可以去源码中查看下DirectedAcyclicGraph
的结构和DFS
算法的实现,在此就不做说明了.
CoordinatorLayout
的绘制过程
CoordinatorLayout
没有重写dispatchDraw
,但是重写了onDraw
和drawChild
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
if (mDrawStatusBarBackground && mStatusBarBackground != null) {
final int inset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
if (inset > 0) {
mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
mStatusBarBackground.draw(c);
}
}
}
ViewGroup
的onDraw
只有在含有background
时才会调用,而且CoordinatorLayout
的处理也只是对于状态栏背景的处理,无足轻重.
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.mBehavior != null) {
final float scrimAlpha = lp.mBehavior.getScrimOpacity(this, child);
if (scrimAlpha > 0f) {
if (mScrimPaint == null) {
mScrimPaint = new Paint();
}
mScrimPaint.setColor(lp.mBehavior.getScrimColor(this, child));
mScrimPaint.setAlpha(MathUtils.clamp(Math.round(255 * scrimAlpha), 0, 255));
final int saved = canvas.save();
if (child.isOpaque()) {