• Android View绘制流程


    框架分析

    在之前的下拉刷新中,小结过触屏消息先到WindowManagerService(Wms)然后顺次传递给ViewRoot(派生自Handler),经decor view到Activity再传递给指定的View,这次整理View的绘制流程,通过源码可知,这个过程应该没有涉及到IPC(或者我没有发现),需要绘制时在UI线程中通过ViewRoot发送一个异步请求消息,然后ViewRoot自己接收并不处理这个消息。

    在正式进入View绘制之前,首先需要明确一下Android UI的架构组成,偷图如下:

     

    上述架构很清晰的呈现了Activity、Window、DecorView(及其组成)、ViewRoot和WMS之间的关系,我通过源码简单理了下从启动Activity到创建View的过程,大致如下

     

    在上图中,performLaunchActivity函数是关键函数,除了新建被调用的Activity实例外,还负责确保Activity所在的应用程序启动、读取manifest中关于此activity设置的主题信息以及上图中对“6.onCreate”调用也是通过对mInstrumentation.callActivityOnCreate来实现的。图中的“8. mContentParent.addView”其实就是架构图中phoneWindow内DecorView里面的ContentViews,该对象是一个ViewGroup类实例。在调用AddView之后,最终就会触发ViewRoot中的scheduleTraversals这个异步函数,从而进入ViewRoot的performTraversals函数,在performTraversals函数中就启动了View的绘制流程。

    performTraversals函数在2.3.5版本源码中就有近六百行的代码,跟我们绘制view相关的可以抽象成如下的简单流程图

     

    流程图中的host其实就是mView,而ViewRoot中的这个mView其实就是DecorView,之所以这么说,又得具体看源码中ActivityThread的handleResumeActivity函数,在这里我就不展开了。上述流程主要调用了View的measure、layout和draw三个函数。

    measure过程分析

    因为DecorView实际上是派生自FrameLayout的类,也即一个ViewGroup实例,该ViewGroup内部的ContentViews又是一个ViewGroup实例,依次内嵌View或ViewGroup形成一个View树。所以measure函数的作用是为整个View树计算实际的大小,设置每个View对象的布局大小(“窗口”大小)。实际对应属性就是View中的mMeasuredHeight(高)和mMeasureWidth(宽)。

    在View类中measure过程主要涉及三个函数,函数原型分别为

    public final void measure(int widthMeasureSpec, int heightMeasureSpec)

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

    前面两个函数都是final类型的,不能重载,为此在ViewGroup派生的非抽象类中我们必须重载onMeasure函数,实现measure的原理是:假如View还有子View,则measure子View,直到所有的子View完成measure操作之后,再measure自己。ViewGroup中提供的measureChild或measureChildWithMargins就是实现这个功能的。

    在具体介绍测量原理之前还是先了解些基础知识,即measure函数的参数由类measureSpec的makeMeasureSpec函数方法生成的一个32位整数,该整数的高两位表示模式(Mode),低30位则是具体的尺寸大小(specSize)。

    MeasureSpec有三种模式分别是UNSPECIFIED, EXACTLY和AT_MOST,各表示的意义如下

    如果是AT_MOST,specSize代表的是最大可获得的尺寸;

    如果是EXACTLY,specSize代表的是精确的尺寸;

    如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。

    那么对于一个View的上述Mode和specSize值默认是怎么获取的呢,他们是根据View的LayoutParams参数来获取的:

    参数为fill_parent/match_parent时,Mode为EXACTLY,specSize为剩余的所有空间;

    参数为具体的数值,比如像素值(px或dp),Mode为EXACTLY,specSize为传入的值;

    参数为wrap_content,Mode为AT_MOST,specSize运行时决定。

    具体测量原理

    上面提供的Mode和specSize只是程序员对View的一个期望尺寸,最终一个View对象能从父视图得到多大的允许尺寸则由子视图期望尺寸和父视图能力尺寸(可提供的尺寸)两方面决定。关于期望尺寸的设定,可以通过在布局资源文件中定义的android:layout_width和android:layout_height来设定,也可以通过代码在addView函数调用时传入的LayoutParams参数来设定。父View的能力尺寸归根到最后就是DecorView尺寸,这个尺寸是全屏,由手机的分辨率决定。期望尺寸、能力尺寸和最终允许尺寸的关系,我们可以通过阅读measureChild或measureChildWithMargins都会调用的getChildMeasureSpec函数的源码来获得,下面简单列表说明下三者的关系

    父视图能力尺寸

    子视图期望尺寸

    子视图最终允许尺寸

    EXACTLY + Size1

    EXACTLY + Size2

    EXACTLY + Size2

    EXACTLY + Size1

    fill_parent/match_parent

    EXACTLY+Size1

    EXACTLY + Size1

    wrap_content

    AT_MOST+Size1

    AT_MOST+Size1

    EXACTLY + Size2

    EXACTLY+Size2

    AT_MOST+Size1

    fill_parent/match_parent

    AT_MOST+Size1

    AT_MOST+Size1

    wrap_content

    AT_MOST+Size1

    UNSPECIFIED+Size1

    EXACTLY + Size2

    EXACTLY + Size2

    UNSPECIFIED+Size1

    fill_parent/match_parent

    UNSPECIFIED+0

    UNSPECIFIED+Size1

    wrap_content

    UNSPECIFIED+0

    上述表格展现的是子视图最终允许得到的尺寸,显然1、4、7三项没有对Size1和Size2进行比较,所以允许尺寸是可以大于父视图的能力尺寸的,这个时候最终的视图尺寸该是多少呢?AT_MOST和UNSPECIFIED的View又该如何决策最终的尺寸呢? 

    通过Demo演示的得到的结果,假如Size2比Size1的尺寸大,假如不使用滚动效果的话,子视图超出部分将被裁剪掉,该父视图中如果在该子视图后面还有其他视图,那么也将被裁剪掉,但是通过调用其getVisibility还是显示该控件是可见的,所以裁剪后控件依然是有的,只是用户没办法观察到;在使用滚动效果的情况下,就能将原本被裁剪掉的控件通过滚动显示出来。

    对于第二个问题,根据源码View的OnMeasure函数调用的getDefaultSize函数获知,默认情况下,控件都有一个最小尺寸,该值可以通过设置android:minHeight和android:minWidth来设置(无设置时缺省为0);在设置了背景的情况下,背景drawable的最小尺寸与前面设置的最小尺寸比较,两者取大者,作为控件的最小尺寸。在UNSPECIFIED情况下就选用这个最小尺寸,其它情况则根据允许尺寸来。不过这个是默认规则,通过demo发现,TextView在AT_MOST+Size情况下,并不是以Size作为控件的最终尺寸,结果发现在TextView的源码中,重载了onMeasure函数,有价值的代码如下:

    ……

    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    ……

    if (widthMode == MeasureSpec.AT_MOST) {

        width = Math.min(widthSize, width);

    }

    ……

    if (heightMode == MeasureSpec.AT_MOST) {

        height = Math.min(desired, heightSize);

    }

    ……

    至于其中的width和desired值,感兴趣的同学可以具体关注下。虽然FrameWork提供了视图默认的尺寸计算规则,但是最终的视图布局大小可以重载onMeasure函数来修改计算规则,当然也可以不计算直接通过setMeasuredDimension来设置(需要注意的是,如果通过setMeasuredDimension的同时还要调用父类的onMeasure函数,那么在调用父类函数之前调用的setMeasuredDimension会无效果)。

    layout过程分析

    上述measure过程达到的结果是设定了视图的高和宽,layout过程的作用就是设定视图在父视图中的四个点(分别对应View四个成员变量mLeft,mTop,mLeft,mBottom)。同样layout也是被fianl修饰符限定为不能重载,不过在ViewGroup中onLayout函数被abstract修饰,即所有派生自ViewGroup的类必须实现onLayout函数,从而实现对其包含的所有子视图的布局设定。

    那么上述的measure结果与layout有什么关系,截取ViewRoot和FrameLayout两个类中onLayout函数的部分代码如下:

    //ViewRoot的performTraversals函数measure之后对layout的调用代码

    host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

    //FrameLayou的onLayout函数部分源码

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

            final int count = getChildCount();

            ……

            for (int i = 0; i < count; i++) {

                final View child = getChildAt(i);

                if (child.getVisibility() != GONE) {

                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                    final int width = child.getMeasuredWidth();

                    final int height = child.getMeasuredHeight();

                    int childLeft = parentLeft;

                    int childTop = parentTop;

                    final int gravity = lp.gravity;

     

                    if (gravity != -1) {

                        final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;

                        final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

     

                        switch (horizontalGravity) {

                            case Gravity.LEFT:

                                childLeft = parentLeft + lp.leftMargin;

                                break;

                            case Gravity.CENTER_HORIZONTAL:

                                childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;

                                break;

                            case Gravity.RIGHT:

                                childLeft = parentRight - width - lp.rightMargin;

                                break;

                            default:

                                childLeft = parentLeft + lp.leftMargin;

                        }

     

                        switch (verticalGravity) {

                            case Gravity.TOP:

                                childTop = parentTop + lp.topMargin;

                                break;

                            case Gravity.CENTER_VERTICAL:

                                childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin;

                                break;

                            case Gravity.BOTTOM:

                                childTop = parentBottom - height - lp.bottomMargin;

                                break;

                            default:

                                childTop = parentTop + lp.topMargin;

                        }

                    }

     

                    child.layout(childLeft, childTop, childLeft + width, childTop + height);

                }

            }

        }

    从代码显然可知具体layout布局时,就是根据measure过程设置的高和宽,结合视图在父视图中的起始位置,再外加视图的layoutgravity属性来设置四个点的具体位置(在LinearLayout中还会增加对layoutweight属性的考虑)。这个过程相对没有measure那么复杂。

    需要注意的是在自定义组合控件的时候,我们可以根据需要不用或只用部分measure过程计算得到的尺寸,具体可以看下之前做的下拉刷新控件直接重载的onLayout函数:

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

        if (getChildCount() > 2) {

            throw new IllegalStateException("NPullToFreshContainer can host only two direct child");

        }

            

        View headView = getChildAt(0);

        View contentView = getChildAt(1);

        if(headView != null){

         headView.layout(0, -HEAD_VIEW_HEIGHT + mTatolScroll, getMeasuredWidth(), mTatolScroll);// mTatolScroll是下拉的位移值

        }

       

        if(contentView != null){

        contentView.layout(0, mTatolScroll, getMeasuredWidth(), getMeasuredHeight());

        }

            

        if (mFirstLayout) {        

         HEAD_VIEW_HEIGHT = getChildAt(0).getMeasuredHeight();

           mFirstLayout = false;

        }

    }

    draw过程分析

    View的Draw过程,其实相对来说应该比measure过程更为复杂,正因为其很复杂,所以android框架层已经将draw过程考虑得相当周全,虽然view类的Draw函数没用final修饰,但是我们自定义的View,一般也不需要去重载实现它,自己目前也没有自己去draw过界面,对整个过程,只能偷别人整理的逻辑,结合源码浏览了一下,在这里做个标注。

    draw()方法实现的功能流程如下:

    1、调用background.draw(canvas)绘制该View的背景

    2、调用onDraw(canvas)方法绘制视图本身(每个View都需要重载该方法,ViewGroup不需要实现该方法)

    3、调用dispatchDraw(canvas)方法绘制子视图(ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,其内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法)

    4、调用onDrawScrollBars(canvas)绘制滚动条

    为了说明measure、layout和draw过程的连续性,摘得draw中的源码如下

    ……

    if (mBackgroundSizeChanged) {

        background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);

        mBackgroundSizeChanged = false;

    }

    ……

    上述的mLeft,mTop,mLeft,mBottom就是我们在layout是设定的结果值,这里之所以要用减法获取高宽尺寸而不用measure过程设定的mMeasuredHeight和mMeasureWidth,个人感觉就是因为我们可以在代码中通过直接调用View的layout函数避开measure测算结果而导致真实高宽不等于mMeasuredHeight和mMeasureWidth这种情况。

    上述代码中的mBackgroundSizeChanged是个私有成员变量,源码中只能在View的onScrollChanged(int l, int t, int oldl, int oldt) 、layout过程调用的setFrame(int left, int top, int right, int bottom) 和setBackgroundDrawable(Drawable d)这三个函数中对其修改为true。

    到这里,除了具体的绘制外,我们对从Activity到View的绘制流程应该比较清楚了。

     

     

     

    本文除了参阅源码,发现下面两篇博文帮助很大,有兴趣可以详细阅读

    http://blog.csdn.net/qinjuning/article/details/7110211

    http://www.cnblogs.com/bastard/archive/2012/04/10/2440577.html

    验证View measure现象的demo见 http://files.cnblogs.com/franksunny/ViewDemo.rar

    由于文档中的图片没有显示出来,所以上传一个pdf文档,方便查阅 http://files.cnblogs.com/franksunny/AndroidView%E7%BB%98%E5%88%B6%E6%B5%81%E7%A8%8B.pdf

     

    « 上一篇:[转]Timer和TimerTask
    » 下一篇:下拉刷新组合控件的制作小结

  • 相关阅读:
    C#操作json
    sql server 2008 身份验证失败 18456
    MD5密码加密
    oracle dg 报错提示 涉及硬盘错误
    Rhel6.5 相关操作
    Centos6.9部署vnc
    Sqluldr2 libclntsh.so报错处理
    时钟服务器同步方法
    windows copy 和xcopy
    Linux 本地repo配置
  • 原文地址:https://www.cnblogs.com/xgjblog/p/4572205.html
Copyright © 2020-2023  润新知