Android中View绘制流程源码分析

2019-12-21  本文已影响0人  MadnessXiong

1. View绘制流程的概览。

    private void performTraversals() {
      
      int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
      int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
      //measure
      mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      //layout
      mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
      //draw
      mView.draw(canvas);
      
    }

这里可以看到依次执行了View的measure(),layout(),draw()。这就是绘制一个View的流程。
那么依次看一下View这3个方法的源码

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
           
            onMeasure(widthMeasureSpec, heightMeasureSpec);
      
          if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }
    }

可以看到,measure()是一个final方法,是我们无法操作的,所以onMeasure()要重点关注的,并且onMeasure()里必须调用setMeasuredDimension(),不然之后就会抛出异常,那么看一下onMeasure()的源码:

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

可以看到,这里只是单纯的调用了setMeasuredDimension(),并传入了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:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }


protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
    
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

可以看到,在getDefaultSize()中通过MeasureSpec.getMode()获取了specMode,通过MeasureSpec.getSize()获取size,这里可以借鉴使用

然后在setMeasuredDimension()中调用了setMeasuredDimensionRaw(),然后在这个方法里最终完成对mMeasuredWidth,mMeasuredHeight的赋值,以及对mPrivateFlags的更新。所以测量的宽高是通过setMeasuredDimension()保存的,同时setMeasuredDimension()后测量的宽高就有值了,可以通过getMeasuredWidth()获取测量的宽,通过getMeasuredHeight()获得测量的高。

小结:measure()是一个final方法,它的方法体里调用了onMeasure(),一般自定义View时可以通过onMeasure()对宽高进行修正,然后在onMeasure()里调用setMeasuredDimension()对测量结果进行保存,所以重写onMeasure()必须调用setMeasuredDimension(),否则无法保存测量结果,并会抛出异常。同时onMeasure()执行完后才能通过getMeasuredWidth()和getMeasuredHeight()获取测量的宽高,因为setMeasuredDimension()是在onMeasure()里执行的

可以通过MeasureSpec.getMode()获取模式,通过MeasureSpec.getSize()获取宽高

    public void layout(int l, int t, int r, int b) {
    //根据传入的值判断布局是否改变过,并调用setFrame()
        boolean changed = setFrame(l, t, r, b);
    //如果布局改变过调用onLayout()
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
        }

     
    }

可以看到,先是通过setFrame(),判断布局是否改变过,如果改变过就调用onLayout(),那么先看一下它们的代码:

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;
    //记录改变后的左上右下的值
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
        return changed;
    }
    
    //空实现
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

可以看到如果左右上下任何一个值发生了改变,那么代表布局发生了改变,改变后对新的左右上下的值进行了记录

然后onLayout是一个空实现。

小结:layout()接收了父View传过来的左上右下四个值,然后在setFrame()里根据这个值是否改变判断了是否更新布局,并且记录了自身最新的左上右下值,如果发生改变那么会调用onLayout()重新布局,需要调用着自己去实现

    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)
         */
       // Step 1, draw the background, if needed
          //如果需要的话绘制背景
          if (!dirtyOpaque) {
              drawBackground(canvas);
             }
      
        // Step 2, save the canvas' layers
        //保存绘制图层
         if (drawTop) {
             canvas.saveLayer(left, top, right, top + length, null, flags);
         }

        // Step 3, draw the content
        //绘制内容
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        //绘制子view
        dispatchDraw(canvas);
      
      // Step 5, draw the fade effect and restore layers
      //绘过渡效果和恢复涂层
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }
      
       // Step 6, draw decorations (scrollbars)
             //绘制滚动条
             onDrawScrollBars(canvas);

    }

可以看到,在源码中官方给出了6个步骤,但是只需要关注步骤3和步骤4就行了,那么分别看一下它们的源码:

    protected void onDraw(Canvas canvas) {
    
    }
    
    protected void dispatchDraw(Canvas canvas) {

    }

都是空实现,onDraw()是用来绘制自己的,dispatchDraw()是用来分发绘制子view的。

小结:draw()中对背景等内容进行了绘制,同时通过onDraw()绘制自身,通过dispatchDraw()分发绘制子View

2. VIew绘制流程细节分析

前面分析了View的大概流程,现在看一下一些细节

测量的时候有一个很重要的概念MeasureSpec,它封装了specMode和specSize,specMode是父View对子VIiew的要求,它有3种模式:
MeasureSpec.EXACTLY:确定模式,父View期望子View的大小是一个已确定的值,也就是这个specSize
MeasureSpec.AT_MOST:至多模式,父View期望子View最大是某个值
MeasureSpec.UNSPECIFIED:未限定模式,父View对子View不做限制,如scrollView

了解了测量模式那么再看一下自定义View时的测量,之前了解到测量是调用measure(),然后可以在onMeasure()中进行修改,现在有2种情况,1是自定义view对自身进行修改,2是测量子view,分别看一下:

2.1.1 通过代码先看修改自身:

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //模拟自定义View自定义的宽高
        int realWidthSize=100;
        int realHeightSize=100;
        //获取mode和size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
                //根据父View对子View的期望mode
        switch (widthMode) {
            //如果父view希望子view是一个确定的值,那么realWidthSize的修改无效,只能是widthSize
            case MeasureSpec.EXACTLY:
                realWidthSize = widthSize;
                break;
            //如果父view希望子view最多是一个值,那么当realWidth最大只能是widthSize
            case MeasureSpec.AT_MOST:
                if (realWidthSize > widthSize) {
                    realWidthSize = widthSize;
                }
                break;
            //如果是未限定模式,那么realWidthSize不变,想多大就多大
            case MeasureSpec.UNSPECIFIED:
                realWidthSize = realWidthSize;
                break;
        }
                //修正完成后通过setMeasuredDimension()保存结果,这里realHeightSize的修正是一样的
        setMeasuredDimension(realWidthSize,realHeightSize);

    }

以上就是对自定义view后对宽高的修正过程,可以看出其实都是模版代码,其实Android已经提供了方法进行修正,看下代码:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //模拟自定义View
        int realWidthSize=100;
        int realHeightSize=100;
                //通过resolveSize()对结果进行修正
        int resolveWidthSize = resolveSize(realWidthSize, widthMeasureSpec);
        int resolveHeightSize = resolveSize(realHeightSize, heightMeasureSpec);
        //保存修正后的结果
        setMeasuredDimension(resolveWidthSize,resolveHeithSize);

    }

2.1.2 再看测量子view:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                //获取子view
        View child = getChildAt(0);
        //获取子view的layoutParams,获取childWidth,然后定义mode
        LayoutParams layoutParams = child.getLayoutParams();
        int childWidth = layoutParams.width;
        int childMode=0;
                //获取mode和size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        switch (widthMode) {
            //如果父view的要求是EXACTLY
            case MeasureSpec.EXACTLY:
                if (childWidth>=0){
                  //如果子view自己设置了具体的值,那么childWidth就是它自身的值,childMode就是EXACTLY
                    childWidth=childWidth;
                    childMode=MeasureSpec.EXACTLY;
                }else if (childWidth==LayoutParams.MATCH_PARENT){
                  //如果子view设置了MATCH_PARENT,那么它的childWidth就等于父view的widthSize,并且父view的mode也是EXACTLY,那么子view的mode也应该是EXACTLY
                    childWidth=widthSize;
                    childMode=MeasureSpec.EXACTLY;

                }else if (childWidth==LayoutParams.WRAP_CONTENT){
                  //如果子view设置了WRAP_CONTENT,那么这里先把父view的widthSize赋值给它的childWidth,并且它是可以在onMeasure()对自己的值进行修改的,那么它的值最大是widthSize,所以它的mode是MeasureSpec.AT_MOST
                    childWidth=widthSize;
                    childMode=MeasureSpec.AT_MOST;
                }
                break;
            //如果父view的要求是AT_MOST
            case MeasureSpec.AT_MOST:
                if (childWidth>=0){
            //如果子view自己设置了具体的值,那么childWidth就是它自身的值,childMode就是EXACTLY
                    childWidth=childWidth;
                    childMode=MeasureSpec.EXACTLY;
                }else if (childWidth==LayoutParams.MATCH_PARENT){
            //如果子view设置了MATCH_PARENT,那么它的childWidth就等于父view的widthSize,但是父view的mode是AT_MOST,那么子view的mode也应该是AT_MOST
                    childWidth=widthSize;
                    childMode=MeasureSpec.AT_MOST;
                }else if (childWidth==LayoutParams.WRAP_CONTENT){
            //如果子view设置WRAP_CONTENT,那么这里先把父view的widthSize赋值给它的childWidth,并且它是可以在onMeasure()对自己的值进行修改的,那么它的值最大是widthSize,所以它的mode是MeasureSpec.AT_MOST
                    childWidth=widthSize;
                    childMode=MeasureSpec.AT_MOST;
                }
                break;
            //如果父view没有要求
            case MeasureSpec.UNSPECIFIED:
                if (childWidth>=0){
            //如果子view自己设置了具体的值,那么childWidth就是它自身的值,childMode就是EXACTLY
                    childWidth=childWidth;
                    childMode=MeasureSpec.EXACTLY;
                }else if (childWidth==LayoutParams.MATCH_PARENT){
            //如果子view设置了MATCH_PARENT,那么它的childWidth就等于父view的widthSize,但是父view的mode是UNSPECIFIED,那么子view的mode也应该是UNSPECIFIED
                    childWidth=widthSize;
                    childMode=MeasureSpec.UNSPECIFIED;
                }else if (childWidth==LayoutParams.WRAP_CONTENT){
            //如果子view设置WRAP_CONTENT,那么这里先把父view的widthSize赋值给它的childWidth,并且它是可以在onMeasure()对自己的值进行修改的,那么它的值最大是widthSize,但是父view的widthSiz并没有限制,所以这里子view的mode也应该是UNSPECIFIED
                    childWidth=widthSize;
                    childMode=MeasureSpec.UNSPECIFIED;
                }
                break;
        
      //根据计算出的size和mode生成MeasureSpec
     int childWidthMeasureSpec=MeasureSpec.makeMeasureSpec(widthMode, widthSize);
     //height的步骤同理,这里省略
     int childWidthMeasureSpec=MeasureSpec.makeMeasureSpec(heightMode, HeightSize)
      //传入MeasureSpec完成测量
      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

以上就是子view的size和mode的计算,最后完成测量的过程,可以看出代码很繁琐,并且也是模版代码,Android也提供了方法:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                View child = getChildAt(0);
        //带margin的测量,一般用这种,这里的参数0是已用空间,可以通过计算已测量的子view进行计算,这里为了方便写为0
        measureChildWithMargins(child,widthMeasureSpec,0,heightMeasureSpec,0);
        //普通测量子view
        measureChild(child,widthMeasureSpec,heightMeasureSpec);
        //测量所有子view,这里里面其实就是遍历了子view然后调用measureChild()
        measureChildren(widthMeasureSpec,heightMeasureSpec);
    }

这里有3个测量方法,其实内里逻辑差不多只是measureChildWithMargins()带了margin,那么在看下这个方法:

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        //这里用到了MarginLayoutParams
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                //通过此方法完成mode和size的计算,这个方法不再展开
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
                //通过计算出的
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

可以看出内部也是计算出了MeasureSpec,最后通过measure()完成测量。需要主要的是如果使用measureChildWithMargins()测量,它里面用到了MarginLayoutParams,需要去实现一个方法,如果不实现,就无法使用layout_margin:

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

小结:

1:自定义View时,可以在onMeasure()中通过resolveSize()对实际的宽高进行修正。如果不对宽高进行修改,那么就是它的父View调用child.measure()测量出的值,这里有个比较特殊的情况就是如果自身设置了wrap_content(),然后不做任何修正的话,那么父view的宽高就是自身的宽高,这个可以从上面的计算过程中看出来。

2:测量子View时可以通过measureChildWithMargins()等3个方法完成对子view的测量,需要注意的是如果使用measureChildWithMargins()需要实现generateLayoutParams()并返回new MarginLayoutParams(getContext(),attrs)

  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;
                        //判断大小是否改变,布局改变不代表大小改变
          boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

                        //记录新的左上右下的值
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
                        
            //如果布局大小改变,调用sizeChange(),然后在这个方法了里又会调用onSizeChanged()
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

        }
        return changed;
    }

这里如果如果布局发生了改变,那么再判断大小是否发生改变,然后给新的左上右下赋值

这里有一个点是左上右下位置的改变,并不代表大小改变,有可能只是位置改变,如果只是位置改变,大小不变那么不会触发onSizeChanged(),否则就会触发onSizeChanged(),并且onSizeChanged()执行时新的位置已经赋值完成,那么这时候这个布局的实际宽高已经有值了,也就是getWidth()和getHeight()可以获取到最新的宽高信息了。

setFrame()完后,如果布局发生了改变或者调用requestLayout(),那么就会调用onLayout(),这是一个空实现,可以在这个方法里通过child.layout(),去对子view进行布局,子view又会重复走以上layout()流程。

小结:layout()主要通过它的setFrame()方法判断了布局是否改变,并且记录了最新的左上右下的值,如果布局发生改变,并且大小改变,那么会调用onSizeChange(),所以一般如果不需要对子view进行操作的话,自身的操作可以放到这个方法里,因为这时已经可以通过getWidth()和getHeigh()拿到真实的宽高值了。如果需要对子view进行操作,那么就重写onLayout(),通过child.layout()去摆放子view的位置,如此一直循环

    public void draw(Canvas canvas) {
        // Step 3, draw the content
        //绘制内容
        //背景不为空且不为透明时才会执行onDraw,viewgroup默认不会执行此方法
        if (!dirtyOpaque) onDraw(canvas)

    }

一般在onDraw()绘制自身内容,另外viewGroup默认不回执行onDraw(),同时onDraw()是一个会调用很多次的方法,所以不要在onDraw()做一些计算或频繁分配内存的工作,一面造成内存抖动。

3. 总结

附:invalidate()和requestLayout()的区别

上一篇下一篇

猜你喜欢

热点阅读