美文网首页
View的绘制流程

View的绘制流程

作者: 锐_nmpoi | 来源:发表于2017-04-06 16:17 被阅读109次

    在Android中View的存在的方式一共有两种形式:

    1. 单一的View控件
    2. 可以包含其他View的ViewGroup

    在了解View的绘制过程的时候,首先就要了解一下我们的Android的UI管理系统的层次关系:

    如图所示:

    UI层次关系.png

    从源码中其实我们很容易就知道每个Activity都会创建一个最基本的窗口系统 PhoneWindow 。 PhoneWindow 是Activity与View 交互的接口。 从图中我们又看到 DecorView , 在事件传递机制下,事件会传递给这个 DecorView 吗,然后子View就能接受到事件了。 在 DecorView 中我们可以看到 TitleView 和 ContentView 。
    TitleView 通常就是 ActionBar ,而 ContentView 就是我们最常接触的,就是平时在 Activity 中通过setContentView() 给Activity设置的View .

    绘制的整体流程

    当一个启动一个Activity的时候,Android系统会根据Activity的布局对它进行绘制。绘制会从根视图ViewRoot的 performTraversals() 方法开始 , 从上往下的遍历整个视图树。然而对于View控件来说,View控件只负责控制自己,而ViewGroup来说,他只是负责通知自己的子View进行绘制。

    ViewRootImpl # performTraversals

      private void performTraversals(){
         
            .....
            //执行测量流程
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
            .....
            //执行布局流程
            performLayout(lp, mWidth, mHeight);
    
            ......
            //执行绘制流程
            performDraw();
           
    }
    

    从ViewRootImpl 中可以看到的就是,视图的绘制会执行以下三个步骤,分别是 Measure (测量) 、Layout(布局)、Draw (绘制) 。

    Measure

    Measure 是用来计算View得到实际大小,由前面的分析可知,页面的绘制是从 performMeasure 方法开始的。

    ViewRootImpl # performMeasure

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

    从上面可以知道,performMeasure方法只是调用了 mView.measure(...) ,把具体的绘制交给了 View 。

    View # measure

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

    View # onMeasure

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

    由上面可以得知到的一点就是performMeasure 最终会调用 View 或者 ViewGroup 的 measure 方法 ,而这里面实际上就是调用了 onMeasure 。

    先对View分析

    对于View来说,当调用到 onMeasure 的方法时候, 如果没有重写这个方法的话,那么默认的调用 getDefaultSize 来获取 View 的宽高。 源码如下:

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

    从上面可以得知:对于View默认是测量很简单,大部分情况就是拿计算出来的MeasureSpec的size 当做最终测量的大小。

    而对于一些派生出来的View ,如TextView 、ImageView 等,它们都对onMeasure方法系统了进行了重写。例如TextView 通常先去会先去测量字符的高度等,然后拿到View本身content这个高度,如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度。

    再对ViewGroup分析

    ViewGroup是特殊的View,然而在ViewGroup里面并没有实现 onMeasure 这个方法。而在不同的派生类中,各自实现了自身的 onMeasure 方法。对于DecorView 来说 ,其实就是一个FrameLayout,对于要测量时,一开始其实就是调用到了 FrameLayout 的 onMeasure 方法中 , 从 FrameLayout 中可以看到:

    FrameLayout # onMeasure

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
    
        .....
    
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
    
        for (int i = 0; i < count; i++) {
            if (mMeasureAllChildren || child.getVisibility() != GONE) {   
                ....
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                ....
            }
        }
    
        ....
         setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        ....
    }
    

    从上面可以看到,其实 ViewGroup 的内部就是 遍历自己的子View,只要不是GONE的都会参与测量。然后等所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高。综上,父View是等所有的子View测量结束之后,再来测量自己。

    Layout

    Layout 过程用来确定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 # layout

    public void layout(int l, int t, int r, int b) {
       .....
        onLayout(changed, l, t, r, b);
       ....
    }
    

    View # onLayout

      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        }
    

    onLayout 实际上就是一个空方法,对于ViewGroup来说,就应该实现这个方法。对于 子ViewGroup 来说,例如LinearLayout、RelativeLayout等,均重写了这个方法。

    Draw

    Draw操作用来将控件绘制出来,源码如下:

    ViewRootImpl # performDraw

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

    ViewRootImpl # draw

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

    ViewRootImpl # drawSoftware

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

    最会就调用子View 的 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
        .....
        drawBackground(canvas);
        ....
        // Step 2, save the canvas' layers
        .....
        saveCount = canvas.getSaveCount();
        .....
    
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
    
        // Step 4, draw the children
        dispatchDraw(canvas);
    
        // Step 5, draw the fade effect and restore layers
        ....
        canvas.drawRect(left, top, left + length, bottom, p);
        ....
        canvas.restoreToCount(saveCount);
    
        ....
    
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
    }
    

    从源码中我们很清晰的看到View绘制的流程

    1. 绘制View的背景
    2. 如果需要,保存canvas,为fading做准备
    3. 绘制View内容
    4. 绘制View的子View
    5. 如果需要的话,绘制View的fading边缘并恢复图层
    6. 绘制View的装饰(如滚动条)

    measure(测量)方法的注意

    从上面我们可以清楚了的明白了View的绘制过程了,从measure到layout再到Draw的一系列过程,最终View绘制了出来。然而有些时候我们想在Activity已启动的时候就做一件任务,这一件任务是获取某个View的宽/高。但是我们在onCreate或者onResume 获取View的宽和高却获取不了数值,测试如下:

     <TextView
        android:id="@+id/tv_main"
        android:layout_width="250dp"
        android:layout_height="35dp"
        android:gravity="center"
        android:text="Hello World!" />
    

    MainActivity

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        mTvMain = (TextView) findViewById(R.id.tv_main);
    
        System.out.println("TextView 的高度为:"+mTvMain.getHeight());
        System.out.println("TextView 的宽度为:"+mTvMain.getWidth());
    }
    

    运行结果如下:

    TextView 的高度为:0
    TextView 的宽度为:0
    

    实际上在onCreate、onStart、onResume中均无法正确得到某
    个View的宽和高信息,这是因为View的measure过程和Activity的生命周期方法不是同步
    执行的因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量
    完毕了。

    如果想要拿取View的宽和高又应怎么做呢?下面介绍三种方法。

    1. onWindowFocusChanged

    onWindowFocusChanged 这个方法的含义是:View已经初始化完毕了,宽/高已经准备
    好了,这个时候去获取宽/高是没问题的。需要注意的是,onWindowFocusChanged会被调
    用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说,当Activity
    继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行onResume
    和onPause,那么onWindowFocusChanged也会被频繁地调用。

    代码如下:

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            System.out.println("TextView 的高度为:"+mTvMain.getHeight());
            System.out.println("TextView 的宽度为:"+mTvMain.getWidth());
        }
    }
    

    运行结果:

    TextView 的高度为:70
    TextView 的宽度为:500
    

    2. view.post(runnable)

    通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable
    的时候,View也已经初始化好了。

    代码如下:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        mTvMain = (TextView) findViewById(R.id.tv_main);
    
        mTvMain.post(new Runnable() {
            @Override
            public void run() {
                System.out.println("TextView 的高度为:"+mTvMain.getHeight());
                System.out.println("TextView 的宽度为:"+mTvMain.getWidth());
            }
        });
    }
    

    运行结果:

    TextView 的高度为:70
    TextView 的宽度为:500
    

    3. ViewTreeObsener

    使用ViewTrecObserver的众多回调可以完成这个功能,比如使用
    OnGlobalLayoutListener 这个接口,当View树的状态发生改变或者View树内部的View的
    可见性发现改变时,onGlobalLayout方法将被回调,因此这是获取View的宽和高一个很好
    的时机。需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次。

    代码如下:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        mTvMain = (TextView) findViewById(R.id.tv_main);
        ViewTreeObserver viewTreeObserver = mTvMain.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mTvMain.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                System.out.println("TextView 的高度为:" + mTvMain.getHeight());
                System.out.println("TextView 的宽度为:" + mTvMain.getWidth());
            }
        });
    }
    

    运行结果:

    TextView 的高度为:70
    TextView 的宽度为:500
    

    相关文章

      网友评论

          本文标题:View的绘制流程

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