View绘制源码浅析(二)布局的测量布局绘制

Posted zhuliyuan丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了View绘制源码浅析(二)布局的测量布局绘制相关的知识,希望对你有一定的参考价值。

前言

在第一篇View绘制源码浅析(一)布局的加载我们知道了setContentView()完成了DecorView的创建,并且将xml中的布局标签转换成了对应的View、属性转换成了对应的LayoutParams然后添加到了id为content的布局上,也就是说完成了布局对象的创建并且和DecorView关联上了。

那么第二篇将介绍View是如何显示出来的,主体流程可分为测量、布局、绘制这三步。

本文源码基于API27,接下来我们开始吧。

概述

绘制的开始是在Activity收到AMS的Resume事件,然后给DecorView设置上ViewRootImpl这个视图结构的顶部对象作为DecorViewparent,然后通过调用ViewRootImplrequestLayout()触发测量、布局、绘制的流程。

对于ViewRootImpl来说是一个包含了父布局功能的视图顶层对象(因为每个View都有parent属性指向父布局,而DecorView已经是最外层的布局了是没有父布局的,所以指向的是ViewRootImpl),不过需要注意它不是一个View。

Activity收到Resume事件后最终会走道ViewRootImplsetView()

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) 
        ...
        requestLayout();//触发测量、布局、绘制
        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                          getHostVisibility(), mDisplay.getDisplayId(),
                                          mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                                          mAttachInfo.mOutsets, mInputChannel);//在window上显示
        ...
    

requestLayout()最终会走到performTraversals()这个方法贼鸡儿长,我们只看重点

    private void performTraversals() 
        ...
        measureHierarchy(host, lp, res,
                         desiredWindowWidth, desiredWindowHeight);//会调到performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)进行测量
        performLayout(lp, mWidth, mHeight);//布局
        performDraw();//绘制
        ...
    

这里也就引出了我们的重点方法

  • performMeasure()测量
  • performLayout()布局
  • performDraw()绘制

接下来我们分别介绍这个过程。

测量

看测量前,我们得先了解一个概念measureSpec,它是一个32位的int值,是父布局的宽高约束和要测量的View的LayoutParams宽高共同作用生成的,作为测量方法的形参传入指导View的测量。其中高2位用于存储测量的模式,低30位用于存储具体的数值。然后为了方便生成和取出这个32位的int值,提供了一个工具类MeasureSpec

    public static class MeasureSpec 
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//高两位的掩码
		//下面是3种模式
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;

      	//传入size和mode生成MeasureSpec
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) 
            if (sUseBrokenMakeMeasureSpec) 
                return size + mode;
             else 
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            
        

		//拿到mode
        @MeasureSpecMode
        public static int getMode(int measureSpec) 
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        

		//拿到size
        public static int getSize(int measureSpec) 
            return (measureSpec & ~MODE_MASK);
        

    

模式一共是有三种,这里先简单介绍下

  1. UNSPECIFIED
    未指定的,大小根据size来决定

  2. EXACTLY
    大小有明确的值,比如width为32或者macth_parent都适用该模式

  3. AT_MOST
    对应上wrap_content这种情况,size为View可用的最大值

通过makeMeasureSpec()可生成32位的measureSpecgetMode()getSize()可拿到mode和size。

准备工作做完了,接下来回到主线看测量方法measureHierarchy()

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) 
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;
		...
        if (!goodMeasure) 
                childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);//获取根布局的MeasureSpec
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);//获取根布局的MeasureSpec
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量方法
                if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) 			
                    windowSizeMayChange = true;
                
            
		...

        return windowSizeMayChange;
    

前面不是说measureSpec是根据父布局的宽高约束和要测量的View的LayoutParams宽高共同作用生成的。而这里稍有不同因为DecorView是最外层的布局了,没有父布局给他生成measureSpec参数所以用window去生成一个。

desiredWindowWidth和desiredWindowHeight是屏幕的宽高,lp.width和lp.height默认是match_parent,接下来看下getRootMeasureSpec()生成的啥。

    private static int getRootMeasureSpec(int windowSize, int rootDimension) 
        int measureSpec;
        switch (rootDimension) 

        case ViewGroup.LayoutParams.MATCH_PARENT://走到这个分支
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        
        return measureSpec;
    

可以看到生成的measureSpec的size为屏幕的尺寸,mode为MeasureSpec.EXACTLY,。然后将measureSpec作为performMeasure()的形参传入。

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) 
        if (mView == null) 
            return;
        
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try 
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);//这里mView为DecorView,即调用DecorView.measure
         finally 
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        
    

这里mView为DecorView为了简便我们直接看FrameLayoutmeasure()方法,而FrameLayout并未重写也没法重写因为是final修饰的,所以最终走到View的measure()

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) 
		...
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;//拿到缓存的key
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);//缓存容器

        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;//如果有PFLAG_FORCE_LAYOUT标记会强制测量

        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;//和旧的MeasuerSpec相比有没有变化
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;//模式是不是EXACTLY
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);//看size有没有变化
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);//如果MeasuerSpec有变化 并且 sAlwaysRemeasureExactly 为true 或者 模式不是Exactly 或者 size有变化

        if (forceLayout || needsLayout) //强制测量或者需要测量
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);//如要强制测量就不用拿缓存了
            if (cacheIndex < 0 || sIgnoreMeasureCache) //没有缓存或者忽略缓存
                onMeasure(widthMeasureSpec, heightMeasureSpec);//onMeasure执行测量逻辑
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
             else //拿缓存
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;//添加需要进行layout的标记
        

        mOldWidthMeasureSpec = widthMeasureSpec;//缓存widthMeasureSpec
        mOldHeightMeasureSpec = heightMeasureSpec;//缓存heightMeasureSpec

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL);
    

从代码中可以看出measure()中进行了条件判断需要满足下列条件其中之一就会触发测量

  • mPrivateFlags包含PFLAG_FORCE_LAYOUT
  • MeasureSpec和旧的相比有变化并且(sAlwaysRemeasureExactly为true一般可以忽略默认是false的,MeasureSpec的mode不为Exactly,MeasureSpec的size和之前测量的值相比有变化)三者满足其一

真正执行测量的是在onMeasure()中,然后我们看到FrameLayoutonMeasure()

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
        int count = getChildCount();//拿到子view数量
        int maxHeight = 0;//最大高度
        int maxWidth = 0;//最大宽度
        int childState = 0;

        for (int i = 0; i < count; i++) //遍历子view
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) //子view不为Gone
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);//测量子View
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);//储存所有子View中最大的宽度
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);//储存所有子View中最大的高度
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) 
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) 
                        mMatchParentChildren.add(child);
                    
                
            
        

        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();//最大宽度加上padding
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();//最大高度加上padding

        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());//和minHeight属性比较取最大值
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());//和minWidth属性比较取最大值

        final Drawable drawable = getForeground();//拿到前景图
        if (drawable != null) 
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());//和前景图比较取最大高度
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());//和前景图比较取最大宽度
        
        //到这里maxHeight和maxWidth所装的值是该FrameLayout显示所有内容需要的宽高

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));//设置测量的宽高
		...
    

FrameLayoutViewGroup所以他的测量是遍历所有子类调用measureChildWithMargins()先完成他们的测量,然后拿到最大的高度和宽度,再加上padding并和前景、min属性做比较确保这个maxHeight和maxWidth拿到的值能显示下所有内容,最后通过resolveSizeAndState()拿到最终的测量结果调用setMeasuredDimension()设置给FrameLayout

这里我们先看他测量子类的measureChildWithMargins()方法。

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) 
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();//拿到子View的lp

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);//根据父MeasureSpec和子view的lp属性共同生成子view的MeasureSpec
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);//根据父MeasureSpec和子view的lp属性共同生成子view的MeasureSpec

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);//触发View的measure方法
    

可以看到在getChildMeasureSpec方法中传入了parentMeasureSpec和子view的lp共同生成的MeasureSpec也印证了我们前面说的。

measureSpec,它是一个32位的int值,是父布局的宽高约束和要测量的View的LayoutParams宽高共同作用生成的

接下来看他如何生成MeasureSpec

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) 
        int specMode = MeasureSpec.getMode(spec);//拿到父布局的mode
        int specSize = MeasureSpec.getSize(spec);//拿到父布局的size

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) 
        case MeasureSpec.EXACTLY://父布局为EXACTLY
            if (childDimension >= 0) //如果子View有明确的值 直接用该值 mode为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.MATCH_PARENT) //如果子view值为match_parent则 resultSize为父布局的size mode为EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.WRAP_CONTENT) //如果子view为WRAP_CONTENT resultSize为父布局的size mode为AT_MOST告知size为最大可用值
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            
            break;

        case MeasureSpec.AT_MOST://父布局为AT_MOST
            if (childDimension >= 0) //如果子View有明确的值 直接用该值 mode为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.MATCH_PARENT) //如果子view值为match_parent则 resultSize为父布局的size mode为AT_MOST因为父布局大小不确定
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
             else if (childDimension == LayoutParams.WRAP_CONTENT) //如果子view为WRAP_CONTENT resultSize为父布局的size mode为AT_MOST告知size为最大可用值
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            
            break;

       //父布局为UNSPECIFIED情况比较少这里就不分析了
        
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);//生成MeasureSpec
    

从代码中可以看到不同情况下生成的MeasureSpec会有差别,不过只要子view宽高有明确值的时候mode一定是Exactly,宽高为wrap_content的时候mode一定是AT_MOST。然后给子View生成完MeasureSpec后调用的child.measure(childWidthMeasureSpec, childHeightMeasureSpec)这个方法我们前面看过,最终会调到View的onMeasure()方法。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    

并未做啥处理直接通过getDefaultSize()获取具体的数值

    public static int getDefaultSize(int size, int measureSpec) 
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) 
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST://在mode为AT_MOST和EXACTLY的时候都是直接取的
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        
        return result;
    

可以看到在mode为AT_MOST和EXACTLY的时候默认取的是MeasureSpec的size,所以我们写自定义view的时候需要重写onMeasure()方法在AT_MOST这种情况下进行测量,不然wrap_content在默认情况下和match_parent没区别,TextView等常用的View都重写了该方法可自行查看。

获取到具体的值后通过setMeasuredDimension()存储。

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) 
		...
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    

    private void 以上是关于View绘制源码浅析(二)布局的测量布局绘制的主要内容,如果未能解决你的问题,请参考以下文章

View绘制流程二:测量布局绘制

View绘制流程二:测量布局绘制

View绘制流程二:测量布局绘制

Android面试收集录12 View测量布局及绘制原理

android基础-viewgroup的测量,布局,绘制

View的绘制流程源码分析