美文网首页
Android中View绘制流程源码分析

Android中View绘制流程源码分析

作者: MadnessXiong | 来源:发表于2019-12-21 20:26 被阅读0次

    1. View绘制流程的概览。

    • 1.1 绘制流程的开始是从ViewRootImpl类的performTraversals() 开始的,看看代码
        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个方法的源码

    • 1.2 measure():
        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()获取宽高

    • 1.3 layout():
        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()重新布局,需要调用着自己去实现

    • 1.4 draw():
        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的大概流程,现在看一下一些细节

    • 2.1 先看measure(), 前面分析过,measure()是一个final方法,那么只能通过onMeasure去完成测量

    测量的时候有一个很重要的概念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)

    • 2.2 再看layout():前面分析过,layout()第一个重要方法是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;
                            //判断大小是否改变,布局改变不代表大小改变
              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的位置,如此一直循环

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

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

    3. 总结

    • view的绘制流程是从ViewRootImpl的performTraversals()开始的,在这个方法里依次调用了measure(),layout(),draw()

    • 自定义view时父view通过调用子view的measure()可以测量子view。然后在子view可以通过重写onMeasure(),对自己进行修改,修改完成后可以通过resolveSize()方法修正宽高,修正后再通过MeasureSpec.makeMeasureSpec()生成MeasureSpec,最后通过setMeasuredDimension()保存结果,这个方法后就可以通过getMeasuredWidth()和getMeasuredHeight()获取测量的宽高了。

    • 在onMeasure()调用子view的measure()方法可以对子view进行测量,可以通过measureChildWithMargins()生成需要的参数,调用这个方法必须实现generateLayoutParams()并返回new MarginLayoutParams(getContext(),attrs)

    • 测量完成后就是layout(),它先是调用setFrame(),在这个方法里如果左上右下的值任何一个改变,那么代表布局改变,同时会记录最新的值,再会判断布局大小是否改变,如果改变,就会调用onSizeChange(),一般如果布局大小改变需要进行的操作可以重写这个方法,它执行时已经能通过getHeight()和getWidth()获取实际的宽高了。

    • setFrame()执行完后会把结果返回给layout(),如果布局改变,那么会去执行onLayout(),这是个空方法,可以在这个方法里调用子view的layout()对子view进行布局。

    • 最后是onDraw()这个方法就是绘制具体内容的方法,所以它会频繁执行,所以需要避免在此频繁分配内存的操作。viewgroup默认不会执行ondraw()

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

    • invalidate()不会执行measure()和layout(),只执行draw(),如果开启了硬件加速则只进行调用者View的重绘。

    • requestLayout()会执行自身及父view的measure(),layout(),一般情况下不会执行draw流程,如果要重绘必须调用invalidate(),或者当布局大小发生改变时layout()中的setFrame()会触发重绘。子View不一定会重新measure()和layout()。

    相关文章

      网友评论

          本文标题:Android中View绘制流程源码分析

          本文链接:https://www.haomeiwen.com/subject/wyngnctx.html