Android 中View的绘制流程(结合图解及伪代码说明)

)##介绍

在Android开发过程中,经常存在需要实现自定义控件的情况,对于比较简单的需求,通过组合系统提供的原生控件既可以完成,但是一旦碰到比较复杂的控件时候,这时候就需要我们亲自动手完成控件的设计,实现对控件的测量、布局、绘制等操作,而这一且操作的前提是你需要了解并掌握View的绘制流程。

在正式讲解View的绘制流程之前,我们有必要先来简单了解下Android的UI管理系统层级关系,如下图所示:

PhoneWindow 是Android系统中最基本的窗口系统,每一个Activity会创建一个。PhoneWindow是Activity和View系统交互的接口。DecorView本质上是一个FrameLayout,是Activity中所有View的祖先。

绘制整体流程

当一个应用启动的时候,会启动一个主Activity,Android系统会根据Activity的布局来对它进行绘制。绘制会从根视图ViewRootImpl的performTraversals()方法开始,从上到下遍历整个视图树,每一个View控件负责绘制自己,而ViewGroup还需要负责通知自己的子View进行绘制操作。整个流程如下图所示:

视图的绘制可以分为三个步骤,分别为测量(Measure)、布局(Layout)和绘制(Draw)。

private void performTraversals() {
    int childWidthMeasureSpec=getRootMeasureSpec(mWidth,lp.width);
    int childHeightMeasureSpec=getRootMeasureSpec(mHeight,lp.height);
    .........
    //执行测量流程
    performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
    .........
    //执行布局流程
    performLayout(lp,desiredWindowWidth,desiredWindowHeight);
    .........
    //执行绘制流程
    performDraw();

}

其框架过程如下:

MeasureSpec

在介绍Measure过程之前,我们先了解一下MeasureSpec,这对之后理解Measure过程是十分重要的!
MeasureSpec表示的是一个32位整型值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec是View类的一个静态内部类,用来说明如何测量这个View,其核心代码如下

 public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}
        //不指定测量模式
        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;

        //根据指定的大小和模式创建一个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);
            }
        }

        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        //获取测量模式
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

       //获取测量大小
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        //微调某一个MeasureSpec的大小
        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }
    }

小结(重点关注代码中的以下三种测量模式):

  • 1、UNSPECIFIED:不指定测量模式,父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。
  • 2、EXACTLY:精确测量模式,当该视图的layout_width或者layout_height指定为具体数值或者match_parent时生效,表示父视图已经决定了子视图的精确大小,这种模式下的View测量值就是SpecSize大小的值。
  • 3、AT_MOST:最大值模式,当该视图的layout_width或者layout_height指定为wrap_content时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。

注意:对DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。

Measure过程

主要作用:

  • 为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。

由上面我们知道,页面的测量流程是从performMeasure()方法开始的,核心代码如下

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

 }

从上面可以看出。具体的测量操作是分发给ViewGroup的,由ViewGroup在它的measureChild方法中传递

measureChildren主要是遍历ViewGroup中的所有View进行测量代码如下

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            // 当View的可见性处于Gone状态时,不对其进行测量
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

measureChild是为了测量某一个指定的View 重要根据父容器的MeasureSpec和子View的LayoutParams等信息计算子View的MeasureSpec

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

measure函数原型为 View.java 该函数不能被重载,而是通过回调onMeasure方法实现的。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    .........
    onMeasure(widthMeasureSpec,heightMeasureSpec);
    .........
 }

onMeasure方法通常是由View特定子类自己实现的,开发者也可以通过重写这个方法实现自定义View。

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

//如果View没有重写onMeasure方法,则会默认直接调用getDefaultSize来获取View的宽高

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;
 }

小结:

具体的调用链如下:

ViewRoot根对象地属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调View/ViewGroup对象的onMeasure()方法,该方法实现的功能如下:

  • 1、设置本View视图的最终大小,该功能的实现通过调用setMeasuredDimension()方法去设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth);
  • 2、如果该View对象是个ViewGroup类型,需要重写该onMeasure()方法,对其子视图进行遍历的measure()过程。
    • 2.1 对每个子视图的measure()过程,是通过调用父类ViewGroup.java类里的measureChildWithMargins()方法去实现,该方法内部只是简单地调用了View对象的measure()方法。(由于measureChildWithMargins()方法只是一个过渡层更简单的做法是直接调用View对象的measure()方法)。

整个measure调用流程就是个树形的递归过程。

为了大家更好的理解采用下面的伪代码

 //回调View视图里的onMeasure过程  
    private void onMeasure(int height , int width){  
     //设置该view的实际宽(mMeasuredWidth)高(mMeasuredHeight)  
     //1、该方法必须在onMeasure调用,否者报异常。  
     setMeasuredDimension(h , l) ;  

     //2、如果该View是ViewGroup类型,则对它的每个子View进行measure()过程  
     int childCount = getChildCount() ;  

     for(int i=0 ;i<childCount ;i++){  
      //2.1、获得每个子View对象引用  
      View child = getChildAt(i) ;  

      //整个measure()过程就是个递归过程  
      //该方法只是一个过滤器,最后会调用measure()过程 ;或者 measureChild(child , h, i)方法都  
      measureChildWithMargins(child , h, i) ;   

      //其实,对于我们自己写的应用来说,最好的办法是去掉框架里的该方法,直接调用view.measure(),如下:  
      //child.measure(h, l)  
     }  
    }  

    //该方法具体实现在ViewGroup.java里 。  
    protected  void measureChildWithMargins(View v, int height , int width){  
     v.measure(h,l)     
    }  

Layout过程

主要作用 :
为将整个根据子视图的大小以及布局参数将View树放到合适的位置上。该过程用来确定View在父容器中的布局位置。

由父容器获取子View的位置参数后,调用子View的layout方法并将位置参数传入实现的,ViewRootImpl的performLayout代码如下:

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    ........
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ........
}
//View.java
public void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴  
 ........
 onLayout(changed, l, t, r, b);
 ........
}
// 空方法,子类如果是ViewGroup类型,则重写这个方法,实现ViewGroup中所有View控件布局流程
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

}

具体的调用链如下:

host.layout()开始View树的布局,继而回调给View/ViewGroup类中的layout()方法。具体流程如下

1、layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ;
2、如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。

同样地, 将上面layout调用流程,用伪代码描述如下:

    // layout()过程  ViewRoot.java  
    // 发起layout()的"发号者"在ViewRoot.java里的performTraversals()方法, mView.layout()  

    private void  performTraversals(){  

        //...  

        View mView  ;  
           mView.layout(left,top,right,bottom) ;  

        //....  
    }  

    //回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现  
    private void onLayout(int left , int top , right , bottom){  

     //如果该View不是ViewGroup类型  
     //调用setFrame()方法设置该控件的在父视图上的坐标轴  

     setFrame(l ,t , r ,b) ;  

     //--------------------------  

     //如果该View是ViewGroup类型,则对它的每个子View进行layout()过程  
     int childCount = getChildCount() ;  

     for(int i=0 ;i<childCount ;i++){  
      //2.1、获得每个子View对象引用  
      View child = getChildAt(i) ;  
      //整个layout()过程就是个递归过程  
      child.layout(l, t, r, b) ;  
     }  
   }  

Draw 过程

Draw操作用来将控件绘制出来,绘制的流程是从performDraw方法开始,核心代码如下。

private void performDraw(){
   ......
   draw(fullRedrawNeeded);
   ......
}

private void draw(boolean fullRedrawNeeded) {
........
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
        return;
    }

........

}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
.......
 mView.draw(canvas);
.......

}

可以看到最终调用到每一个View的Draw方法绘制每一个具体的View,绘制基本上可以分为六个步骤,代码如下

public void draw(Canvas canvas) {
    ........
   //步骤一:绘制View的背景
   drawBackground(canvas);
   .........
   //步骤二:如果需要的话,保存canvans的图层,为fading做准备
   saveCount = canvas.getSaveCount();
   ........
   canvas.saveLayer(left, top, right, top + length, null, flags);

   //步骤三:绘制View的内容
   onDraw(canvas);
   .....
   //步骤四:绘制View的子View
    dispatchDraw(canvas);
//步骤五:如果需要的话,绘制View的fading边缘并恢复图层
    canvas.drawRect(right - length, top, right, bottom, p);
    ......
   canvas.restoreToCount(saveCount);
    ......
  //步骤六:绘制View的装饰(比如滚动条)
  onDrawScrollBars(canvas);

}

小结

由ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重绘时,就会为该View添加该标志位。

调用流程:

mView.draw()开始绘制,draw()方法实现的功能如下:

  • 1、绘制该View的背景
  • 2、为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)
  • 3、调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)
  • 4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)值得说明的是,ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。
    • 4.1 dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个地方“需要重绘”的视图才会调用draw()方法)。

伪代码:

    // draw()过程     ViewRoot.java  
    // 发起draw()的"发号者"在ViewRoot.java里的performTraversals()方法, 该方法会继续调用draw()方法开始绘图  
    private void  draw(){  

        //...  
     View mView  ;  
        mView.draw(canvas) ;    

        //....  
    }  

    //回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现  
    private void draw(Canvas canvas){  
     //该方法会做如下事情  
     //1 、绘制该View的背景  
     //2、为绘制渐变框做一些准备操作  
     //3、调用onDraw()方法绘制视图本身  
     //4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。  
          // 应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。  
     //5、绘制渐变框    
    }  

    //ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法  
    @Override  
    protected void dispatchDraw(Canvas canvas) {  
     //   
     //其实现方法类似如下:  
     int childCount = getChildCount() ;  

     for(int i=0 ;i<childCount ;i++){  
      View child = getChildAt(i) ;  
      //调用drawChild完成  
      drawChild(child,canvas) ;  
     }       
    }  
    //ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法  
    protected void drawChild(View child,Canvas canvas) {  
     // ....  
     //简单的回调View对象的draw()方法,递归就这么产生了。  
     child.draw(canvas) ;  
     //.........  
    }  

强调一点的就是,在这三个流程中,Google已经帮我们把draw()过程框架已经写好了,自定义的ViewGroup只需要实现measure()过程和layout()过程即可 。

这三种情况,最终会直接或间接调用到三个函数,分别为invalidate(),requsetLaytout()以及requestFocus() ,接着这三个函数最终会调用到ViewRoot中的schedulTraversale()方法,该函数然后发起一个异步消息,消息处理中调用performTraverser()方法对整个View进行遍历。

invalidate()方法

说明:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。

一般引起invalidate()操作的函数如下:

  • 1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。
  • 2、setSelection()方法 :请求重新draw(),但只会绘制调用者本身。
  • 3、setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,继而绘制该View。
  • 4、setEnabled()方法 : 请求重新draw(),但不会重新绘制任何视图包括该调用者本身。

requestLayout()方法

说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括该调用者本身。

一般引起invalidate()操作的函数如下:

  • 1、setVisibility()方法:当View的可视状态在INVISIBLE/ VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。

requestFocus()方法

说明:请求View树的draw()过程,但只绘制“需要重绘”的视图。

总结

至此View绘制流程基本讲述完毕,为了更好的巩固这些知识,可以参考我的另一篇文章Android中自定义控件之流式布局实现方式

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值