Android View绘制流程

本文详细阐述了从启动Activity到创建View的整个UI绘制流程,包括框架分析、UI架构组成、从Activity到View的绘制过程,重点解析了measure、layout和draw过程,以及自定义组合控件的实现。

框架分析

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

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

 

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

 

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

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

 

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

measure过程分析

因为DecorView实际上是派生自FrameLayout的类,也即一个ViewGroup实例,该ViewGroup内部的ContentViews又是一个ViewGroup实例,依次内嵌ViewViewGroup形成一个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,则measureView,直到所有的子View完成measure操作之后,再measure自己。ViewGroup中提供的measureChildmeasureChildWithMargins就是实现这个功能的。

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

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

如果是AT_MOSTspecSize代表的是最大可获得的尺寸;

如果是EXACTLYspecSize代表的是精确的尺寸;

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

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

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

参数为具体的数值,比如像素值(pxdp),ModeEXACTLYspecSize为传入的值;

参数为wrap_contentModeAT_MOSTspecSize运行时决定。

具体测量原理

上面提供的ModespecSize只是程序员对View的一个期望尺寸,最终一个View对象能从父视图得到多大的允许尺寸则由子视图期望尺寸和父视图能力尺寸(可提供的尺寸)两方面决定。关于期望尺寸的设定,可以通过在布局资源文件中定义的android:layout_widthandroid:layout_height来设定,也可以通过代码在addView函数调用时传入的LayoutParams参数来设定。父View的能力尺寸归根到最后就是DecorView尺寸,这个尺寸是全屏,由手机的分辨率决定。期望尺寸、能力尺寸和最终允许尺寸的关系,我们可以通过阅读measureChildmeasureChildWithMargins都会调用的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

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

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

对于第二个问题,根据源码ViewOnMeasure函数调用的getDefaultSize函数获知,默认情况下,控件都有一个最小尺寸,该值可以通过设置android:minHeightandroid:minWidth来设置(无设置时缺省为0);在设置了背景的情况下,背景drawable的最小尺寸与前面设置的最小尺寸比较,两者取大者,作为控件的最小尺寸。在UNSPECIFIED情况下就选用这个最小尺寸,其它情况则根据允许尺寸来。不过这个是默认规则,通过demo发现,TextViewAT_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);

}

……

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

layout过程分析

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

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

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

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

//FrameLayouonLayout函数部分源码

    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过程分析

ViewDraw过程,其实相对来说应该比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)绘制滚动条

为了说明measurelayoutdraw过程的连续性,摘得draw中的源码如下

……

if (mBackgroundSizeChanged) {

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

    mBackgroundSizeChanged = false;

}

……

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

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

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值