Android View绘制3 Layout流程

一 概述

在上篇文章中我们分析了 View 的 Measure 过程,本篇文章我们分析 Layout 过程,通过本篇文章,你将了解到:

  • 关于 Layout 简单类比
  • 一个简单 Demo
  • View Layout 过程
  • ViewGroup Layout 过程
  • View/ViewGroup 常用方法分析
  • 为什么说 Layout 是承上启下的作用

二 关于Layout简单类比

在上篇文章的比喻里,我们说过:

老王给三个儿子,大王(大王儿子:小小王)、二王、三王分配了具体的良田面积,三个儿子(小小王)也都确认了自己的需要的良田面积。这就是:Measure 过程

既然知道了分配给各个儿孙的良田大小,那他们到底分到哪一块呢,是靠边、还是中间、还是其它位置呢?先分给谁呢?

老王想按到这个家的时间先后顺序来分 (对应 addView 顺序),大王是自己的长子,先分配给他,于是从最左侧开始,划出3亩田给大王。现在轮到二王了,由于大王已经分配了左侧的3亩,那么给二王的5亩地只能从大王右侧开始划分,最后剩下的就分给三王。这就是:ViewGroup onLayout 过程。

大王拿到老王给自己指定的良田的边界,将这个边界 (左、上、右、下) 坐标记录下来。这就是:View Layout 过程。

接着大王告诉自己的儿子小小王:你爹有点私心啊,从爷爷那继承的5亩田地不能全分给你,我留一些养老。这就是设置:padding 过程。

如果二王在最开始测量的时候就想:我不想和大王、三王的田离得太近,那么老王就会给大王、三王与二王的土地之间留点缝隙。这就是设置:margin 过程

三 一个简单Demo

自定义 ViewGroup

public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int usedWidth = 0;
        int maxHeight = 0;
        int childState = 0;

        //测量子布局
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            MarginLayoutParams layoutParams =
                (MarginLayoutParams) childView.getLayoutParams();
            measureChildWithMargins(childView, widthMeasureSpec, usedWidth,
                heightMeasureSpec, 0);
            usedWidth += layoutParams.leftMargin + layoutParams.rightMargin +
                childView.getMeasuredWidth();
            maxHeight = Math.max(maxHeight, layoutParams.topMargin +
                layoutParams.bottomMargin + childView.getMeasuredHeight());
            childState = combineMeasuredStates(childState, childView.getMeasuredState());
        }

        //统计子布局水平,记录尺寸值
        usedWidth += getPaddingLeft() + getPaddingRight();
        maxHeight += getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(
            resolveSizeAndState(usedWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                childState << MEASURED_HEIGHT_STATE_SHIFT));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //父布局传递进来的位置信息
        int parentLeft = getPaddingLeft();
        int left = 0;
        int top = 0;
        int right = 0;
        int bottom = 0;

        //遍历子布局
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            MarginLayoutParams layoutParams =
                (MarginLayoutParams) childView.getLayoutParams();
            left = parentLeft + layoutParams.leftMargin;
            right = left + childView.getMeasuredWidth();
            top = getPaddingTop() + layoutParams.topMargin;
            bottom = top + childView.getMeasuredHeight();
            //子布局摆放
            childView.layout(left, top, right, bottom);
            //横向摆放
            parentLeft += right;
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParam(getContext(), attrs);
    }

    //自定义LayoutParams
    static class MyLayoutParam extends MarginLayoutParams {
        public MyLayoutParam(Context c, AttributeSet attrs) {
            super(c, attrs);
        }
    }

该 ViewGroup 重写了onMeasure() 和 onLayout() 方法:

  • onMeasure() 测量子布局大小,并根据子布局测算结果来决定自己的尺寸
  • onLayout() 摆放子布局位置

同时,当 layout 执行结束,清除 PFLAG_FORCE_LAYOUT 标记,该标记会影响 Measure 过程是否需要执行 onMeasure。

自定义View

public class MyView extends View {

    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int defaultSize = 100;
        setMeasuredDimension(
            resolveSize(defaultSize, widthMeasureSpec),
            resolveSize(defaultSize, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.GREEN);
    }
}

该 View 重写了 onMeasure() 和 onLayout() 方法:

  • onMeasure() 测量自身大小,并记录尺寸值
  • onLayout() 什么都没做

MyViewGroup 添加子布局

<?xml version="1.0" encoding="utf-8"?>
<com.fish.myapplication.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myviewgroup"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_gravity="center_vertical"
    android:background="#000000"
    android:paddingLeft="10dp"
    tools:context=".MainActivity">
    <com.fish.myapplication.MyView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    </com.fish.myapplication.MyView>

    <Button
        android:layout_marginLeft="10dp"
        android:text="hello Button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    </Button>
</com.fish.myapplication.MyViewGroup>

MyViewGroup 里添加了 MyView、Button 两个控件,最终运行的效果如下:
在这里插入图片描述

可以看出,MyViewGroup 里子布局的是横向摆放的。我们重点关注 Layout 过程。实际上,MyViewGroup 里我们只重写了 onLayout() 方法,MyView 也是重写了 onLayout() 方法。接下来,分析 View Layout 过程。

四 Layout过程

与 Measure 过程类似,连接 ViewGroup onLayout() 和 View onLayout() 之间的桥梁是 View layout()。

View.java

    public void layout(int l, int t, int r, int b) {
        //PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT 在measure时候可能会设置
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        //记录当前的坐标值
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        //新(父布局给的)的坐标值与当前坐标值不一致,则认为有改变
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        //坐标改变或者是需要重新layout
        //PFLAG_LAYOUT_REQUIRED 是Measure结束后设置的标记
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) ==
            PFLAG_LAYOUT_REQUIRED) {
            //调用onLayout方法,传入父布局传入的坐标
            onLayout(changed, l, t, r, b);
            ......
            //清空请求layout标记
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            //监听的onLayoutChange回调,通过addOnLayoutChangeListener设置
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(
                        this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        
        final boolean wasLayoutValid = isLayoutValid();
        //清空强制布局标记,该标记在measure时判断是否需要onMeasure;
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        ......
    }
    

    public static boolean isLayoutModeOptical(Object o) {
        //设置了阴影,发光等属性
        //只有ViewGroup有这属性
        //设置    android:layoutMode="opticalBounds" 或者    android:layoutMode="clipBounds"
        //则返回true,默认没设置以上属性
        return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
    }


    private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        //如果设置了阴影、发光灯属性
        //则获取其预留的尺寸
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        //重新改变坐标值,并调用setFrame(xx)
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }

可以看出,最终都调用了 setFrame() 方法。

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        //当前坐标值与新的坐标值不一致,则重新设置
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            //记录PFLAG_DRAWN标记位
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            
            //记录新、旧宽高
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            //新、旧宽高是否一样
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            //不一样,走inValidate,最终执行Draw流程
            invalidate(sizeChanged);

            //将新的坐标值记录
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            //设置坐标值给RenderNode
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            //标记已经layout过
            mPrivateFlags |= PFLAG_HAS_BOUNDS;
            
            if (sizeChanged) {
                //调用sizeChange,在该方法里,我们已经能够拿到View宽、高值
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }
            ......
        }
        return changed;
    }

对于 Measure 过程在 onMeasure() 里记录了尺寸的值,而对于 Layout 过程则在 layout() 里记录了坐标值,具体来说是在 setFrame() 里,该方法有两个地方需要重点关注:

  • 将新的坐标值记录到成员变量 mLeft、mTop、mRight、mBottom 里
  • 将新的坐标值记录到 RenderNode 里,当调用 Draw 过程的时候,Canvas 绘制起点就是 RenderNode 里的位置

View.onLayout()

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

View.onLayout() 是空实现,从 layout() 和 onLayout() 声明可知,这两个方法都是可以被重写的,接下来看看 ViewGroup 是否重写了它们。

ViewGroup Layout过程

ViewGroup.java

    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            //没有被延迟,或者动画没在改变坐标
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            //父类方法,其实就是View.layout()
            super.layout(l, t, r, b);
        } else {
            //被延迟,那么设置标记位,动画完成后根据标志位requestLayout,重新发起layout过程
            mLayoutCalledWhileSuppressed = true;
        }
    }

ViewGroup.layout() 虽然重写了 layout(),但仅仅只是做了简单判断,最后还是调用了 View.layout()。

ViewGroup.onLayout()

ViewGroup.java

    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

重写后将 onLayout 变为抽象方法,也就是说继承自 ViewGroup 的类必须重写 onLayout() 方法。我们以 FrameLayout 为例,分析其 onLayout() 做了什么。

FrameLayout.onLayout()

FrameLayout.java

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        //子布局个数
        final int count = getChildCount();
        //前景padding,意思是子布局摆放的时候不要侵占该位置
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();
        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        //遍历子布局
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //GONE状态下无需layout
            if (child.getVisibility() != GONE) {
                //获取LayoutParams
                final FrameLayout.LayoutParams lp =
                    (FrameLayout.LayoutParams) child.getLayoutParams();
                //获取之前在Measure过程确定的测量值
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                
                int childLeft;
                int childTop;
                //摆放重心落在哪
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                //布局方向,左到右还是右到左,默认左到右
                final int layoutDirection = getLayoutDirection();
                //水平反向的Gravity
                final int absoluteGravity =
                    Gravity.getAbsoluteGravity(gravity, layoutDirection);
                //垂直方向的Gravity
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        //若子布局水平居中,则它的水平方向起始点
                        //扣除父布局padding剩下的位置
                        //结合子布局宽度,使得子布局在剩下位置里居中
                        //再将子布局margin考虑进去
                        //从这里可以看出,若是xml里有居中,也有margin,先考虑居中,然后再考虑margin
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                                lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        //靠右,则改变横向的开始坐标值
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                        //默认是从左到右
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                //垂直方向与水平方向类似
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                                lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                
                //确定了child的坐标位置
                //传递给child
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

FrameLayout 布局 Layout 的时候,起始坐标都是以 FrameLayout 为基准,并没有记录上一个子布局占了哪块位置,因此子布局的摆放位置可能会重叠,这也是 FrameLayout 布局特性的由来。而我们之前的 Demo 在水平方向上记录了上一个子布局的摆放位置,下一个摆放时只能在它之后,因此就形成了水平摆放的功能。由此可以知道,我们常说的某个子布局在父布局里的哪个位置,决定这个位置的即是 ViewGroup.onLayout()。

4.1 Layout调用关系

上面我们分析了 View.layout()、View.onLayout()、ViewGroup.layout()、ViewGroup.onLayout(),那么这四者是什么关系呢?

View.layout()

更新位置坐标,并根据条件调用 onLayout,同时将摆放位置坐标记录到成员变量里并给 RenderNode 设值

View.onLayout()

空实现

ViewGroup.layout()

调用 View.layout()

ViewGroup.onLayout()

抽象方法,子类必须重写。子类重写时候需要为每一个子布局计算出摆放位置,并传递给子布局

View/ViewGroup 子类需要重写哪些方法

  • 继承自 ViewGroup 必须重写 onLayout(),为子布局计算位置坐标
  • 继承自 View 无需重写 layout() 和 onLayout(),因为它已经没有子布局可以摆放

4.2 Layout的承上启下作用

通过以上的分析,我们发现 Measure 过程和 Layout 过程里定义的方法比较类似:

measure() <-----> layout()
onMeasure() <-----> onLayout()

他们的实现流程比较类似:measure()、layout() 一般不需要我们重写,measure() 里调用 onMeasure(),layout() 里调用 onLayout()。

ViewGroup.onMeasure() 里遍历子布局,并测量每个子布局,最后将结果汇总,设置自己测量的尺寸;onLayout() 里遍历子布局,并设置每个子布局的坐标。

View.onMeasure() 则测量自身,并存储测量尺寸;onLayout() 不需要做什么。

承上

Measure 过程虽然比 Layout 过程复杂,但仔细分析后就会发现其本质就是为了设置两个成员变量:

  • 设置 mMeasuredWidth 和 mMeasuredHeight

而 Layout 过程虽然比较简单,其本质是为了设置坐标值

  • 设置 mLeft、mRight、mTop、mBottom 这四个值确定一个矩形区域
  • mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom) 给 RnederNode 设置坐标

将 Measure 设置的变量和 Layout 设置的变量联系起来:

  • mRight、mBottom 是根据 mLeft、mTop 结合 mMeasuredWidth、mMeasuredHeight 计算而得的

此外,Measure 过程通过设置 PFLAG_LAYOUT_REQUIRED 标记来告诉需要进行 onLayout,而 Layout 过程通过清除 PFLAG_FORCE_LAYOUT 来告诉 Measure 过程不需要执行 onMeasure 了。

以上就是 Layout 的承上作用。

启下

我们知道 View 的绘制需要依靠 Canvas 绘制,而 Canvas 是有作用区域限制的。例如我们使用:

canvas.drawColor(Color.GREEN);

Cavas 绘制的起点是哪呢?对于硬件绘制加速来说是通过 Layout 过程中设置的 RenderNode 坐标,而对于软件绘制来说:

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        canvas.translate(mLeft - sx, mTop - sy);
    }

关于硬件绘制加速/软件绘制后续文章会分析。

这就是 Layout 的启下作用,以上即是 Measure、Layout、Draw 三者的内在联系。当然 Layout 的"承上"还需要考虑 margin、gravity 等参数的影响。具体用法参见最开始的 Demo。

五 经典问题

getMeasuredWidth() / getMeasuredHeight 与 getWidth/getHeight 区别

我们以获取 width 为例,分别来看看其方法:

View.java

    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

    public final int getWidth() {
        return mRight - mLeft;
    }
  • getMeasuredWidth():获取测量的宽,属于"临时值",measure 后才能获取
  • getWidth():获取 View 真实的宽,layout 后才能获取

在 Layout 过程之前,getWidth() 默认为 0

何时可以获取真实的宽、高?

  • 重写 View.onSizeChanged() 方法获取
  • 注册 View.addOnLayoutChangeListener(),在 onLayoutChange() 里获取
  • 重写 View.onLayout() 方法获取

下篇将分析 Draw() 过程,如有问题欢迎留言评论交流。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值