Android CoordinatorLayout和Behavior解析

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的BehavioronMeasureChild方法代替,这感觉就像被黑客劫持了一样,子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的LayoutParamsGravity设置坐标;
如果子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先布局而创建了这个列表.这个列表如何排序生成的呢?源码中在CoordinatorLayoutonMeasure中的prepareChildren中生成一个无回路有向图(DirectedAcyclicGraph),然后使用深度优先遍历算法(DFS)将图遍历出来,再进行反序处理(Collections.reverse)生成的,对算法比较感兴趣的可以去源码中查看下DirectedAcyclicGraph的结构和DFS算法的实现,在此就不做说明了.

CoordinatorLayout的绘制过程

CoordinatorLayout没有重写dispatchDraw,但是重写了onDrawdrawChild

    @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);
            }
        }
    }

ViewGrouponDraw只有在含有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()) {
                    
  • 20
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值