view测量布局绘制的源码分析

view的绘制顺序是 measure layout draw。但是具体内部实现原理却不是很清楚,今天就对这整套流程来进行源码分析。

1.measure测量流程

a.起点ViewRootImpl中的performMeasure()方法

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
      mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}

这两个参数,childWidthMeasureSpec和childHeightMeasureSpec
分别来源于

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

通过两个参数,得到根view的测量规格。

系统将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec(规格),然后在onMeasure中根据这个MeasureSpec来确定view的测量宽高。

我们通常在xml文件里面 会给布局添加宽高对应的模式,也就是match_parent和wrap_content再或者是自定义的大小,它的作用,告诉父容器,是要根据父容器来适配 还是要根据内容来适配。
先根据对应的模式,再加上对应的值 ,来确定最终的宽高。

总的来说就是:显示模式+实际值,来确定measure的值
高的显示模式 match_parent和值
宽的显示模式match_parent和值

我们再对这个传进来的参数进行分析
mWidth 在构造中就会赋值为 -1,之后具体的赋值暂时不管。
lp.width 这里的lp代表WindowManager.LayoutParams lp = mWindowAttributes;
资源文件,xml的信息,也就是Decorview中默认加载的那个xml

GetRootMeasureSpec()方法实现 规则产生对应的规格对象,onMeasure里面是根据measureSpec来确定高度的。

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实际上是有三种规格

源码反码补码

深入理解计算机系统

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    //int是4个字节,8个字符,十六进制就是32位。
    //0x代表16进制,16进制的3 ,转换成10进制, 3*16的0次方,还是3。再转换成2进制
    //3==>0000 0000 0000 0000 0000 0000 0000 0000 00 11
    //左移30, 变成
   //110000 0000 0000 0000 0000 0000 0000 0000 00 
    最高位是符号位,代表是负数,同时,计算机存储的都是补码,所以需要取反,加1。得到原码。

//110000 0000 0000 0000 0000 0000 0000 0000 00 补码
//101111 1111 1111 1111 1111 1111 1111 1111 11 (取反,首位因为是负数,所以不取反,其他取反)
//0 00000 0000 0000 0000 0000 0000 0000 0000  01(加1)

//  1 01111 1111 1111 1111 1111 1111 1111 1111 11 
//+ 0 00000 0000 0000 0000 0000 0000 0000 0000  01    
//最终的值为 11 0000 0000 0000 0000 0000 0000 0000 0000  00
//最高位是符号位,不参与计算,代表负数,也就是2的三十次方。就是-2的三十次方,得到-1073741824
//这方面基础较差的朋友可以看下《深入理解计算机系统》

//这样做的好处就是二进制计算速度快,而且一个值比两个值占的空间小。

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
   //自己自定义,最终的值是0
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //对应match_parent,最终值为2的30次方
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    //对应wrap_content,最终值为2的31次方(得到补码的值)
}

    //就是将大小和Spec进行打包。
    //实际上这种计算规则就是 模式+具体数值--》规格  
    //数据格式32位,左两位代表模式,右30代表具体数值
    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和size功能
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

得到根布局的规格,对应关系分别是
MeasureSpec.EXACTLY<==> match_parent
MeasureSpec.AT_MOST<==>wrap_content
MeasureSpec.UNSPECIFIED <==>常用于可滑动的view(如listview或者Recycleview等)

说白了也就是一种转换规则
要么跟父容器的大小 match-parent,
要么就是子控件的大小wrapContent,
要么就是自己自定义的大小。

同时这里需要理解,最初的xml的父容器如果是match_parent,它的parent实际上是DecorView默认加载进去的xml。

这个值会给到onMeasure,onMeasure是根据这个规格来确定view的宽高。
就是转换规则+具体大小,才是真实的宽高。

宽高得到交给performMeasure()方法,再会给到 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
从view.measure中会调用onMeasure()方法,内部setMeasureDimension(),该方法用于保存测量结果.这个只是给view来调用的。

总结:

最关键的其实是这个onMeasure(),我们所有的系统组件,无论是LinearLayout还是framelayout,都是重新的onMeasure这个方法而已。本质都是view,测量完之后,再onLayout进行摆放,最后再绘制而已,在onMeasure()中写一些业务代码。

2.layout流程

开始:performLayout(lp, mWidth, mHeight);//资源,宽,高

   private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {
    final View host = mView;//这里的view实际上就是DecorView
    if (host == null) {
        return;
    }
   host.layout(0, 0, host.getMeasuredWidth(),     
   host.getMeasuredHeight());
   }


public void layout(int l, int t, int r, int b) {
    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);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);//这里调用了onLayout
    }

而这个onLayout()方法的实现是个空的。

protected void onLayout(boolean changed, int left, int top, int right,   
 int bottom) {
   //提供给外部,加业务代码  
 }

总结:
顶层的view调用layout,然后公开onLayout出去让别人重写,添加自身的业务,可以以FrameLayout的onLayout方法为例,内部自己去调用子View的layout方法,然后将子View点算完给他,让他自己去摆放 所有的布局摆放,实际上就是每个view都有个layout去排版,排版会调用onLayout

流式布局来验证这些结论,但是这里只是一个demo,只是用于这些知识点,实际用于项目的 ,还需要另外进行计算。

注意事项

在7.0之后和7.0之前系统对于源码由一定程度的改变
7.0会导致onLayout可能会调用两次。

7.0的performTraversals() 内部有这样一行代码,会导致onMeasure()调用两次
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();//这里又会重新开始绘制流程(需要画一个流程图)
}

ondraw内部又会调用 if (animating)
{
mFullRedrawNeeded = true;
scheduleTraversals();
}

onMeasure,onMeasure,onLayout
onMeasure,onLayout,onMeasure,onLayout
onMeasure会调用两次。因为源码内部流程是重新走了一遍流程,具体是为什么 以及怎么写的,不太清楚。

3.performDraw()流程==>drawSoftware()==>draw()==>ondraw()

draw()方法中的

private void draw(boolean fullRedrawNeeded) {
    Surface surface = mSurface;//实际上就是一个画布
    final Rect dirty = mDirty;//定一个矩阵,实际上就是在屏幕中去确认绘制范围。
    //中间一大堆矩阵大小范围的源码略
     if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                return;
    }

而drawSoftware中 则会调用mview.draw()。

  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
        boolean scalingRequired, Rect dirty) {
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;
        final Canvas canvas;
        canvas = mSurface.lockCanvas(dirty);//这里是调用底层C的代码,实际上的画布是Surface,而canvas实际上是类似于一个代理者,决定了图形的位置,形状等特性
        mView.draw(canvas);
        final Paint p = scrollabilityCache.paint;//paint决定了色彩和样式
}

mView.draw(canvas);才调用了ondraw

public void draw(Canvas canvas) {
/*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */
                绘制的步骤:1.画background
                          2.如果必要的话,保存canvas,用于渐变
   (图形在绘制时,实际上是一层层叠加的,先保存当前层,第二次绘画,就重新在上面叠加一层,
  涉及到这样的步骤。所以我们在定位,如果定在某一层,则会把下面的层级给覆盖掉)
                          3.画view的内容
                          4.画子view
                          5.如果必要,绘制渐变恢复
                          6.绘制装饰(例如滚动条)
   // skip step 2 & 5 if possible (common case)
   // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);
    // Step 4, draw the children
        dispatchDraw(canvas);
    //Step 5
        drawAutofilledHighlight(canvas);
        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }

step1:drawBackground()源码分析:

private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    setBackgroundBounds();//设置背景边框
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);//为原点就开始绘制
    } else {
        canvas.translate(scrollX, scrollY);//不是原点就回到原点进行绘制
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

step3: onDraw() 源码分析:

   /**
 * Implement this to do your drawing.
 *
 * @param canvas the canvas on which the background will be drawn
 */
protected void onDraw(Canvas canvas) {
      //实际上就是对外提供方法
}

step4:dispatchDraw()源码分析,绘画子view

 /**
 * Called by draw to draw the child views. This may be overridden
 * by derived classes to gain control just before its children are drawn
 * (but after its own view has been drawn).
 *
 * @param canvas the canvas on which to draw the view
 */
protected void dispatchDraw(Canvas canvas) {
  //提供给子view的,最典型的就是ViewGroup的体系,如listview
}

step5: drawAutofilledHighlight()

//恢复图层,理解成save和restore就好了。。实际上是涉及到底层的OpenGL以及优化层面的东西,图形的绘制都是GPU来处理的
private void drawAutofilledHighlight(@NonNull Canvas canvas) {
    if (isAutofilled()) {
        Drawable autofilledHighlight = getAutofilledDrawable();

        if (autofilledHighlight != null) {
            autofilledHighlight.setBounds(0, 0, getWidth(), getHeight());
            autofilledHighlight.draw(canvas);
        }
    }
}

step 6和7就是 绘制view的装饰

FlowLayout源码分析帮助理解measureSpec

igding wechat
2018 依计行事 持续精进
坚持原创技术分享,您的支持将鼓励我继续创作!