View工作原理

上一篇博客主要讲了View的事件体系,主要就View参数的相关概念、滑动以及事件分发进行了详细说明,这次主要看下View的工作原理。主要就基本概念及三大绘制流程进行简要分析,文章主要理论知识来自 Android开发艺术探索

1.    ViewRoot和DecorView

当Activity调用setContentView方法后会调用PhoneWindow类的setContentView方法,PhoneWindow是Window的实现类,Window类用来描述Activity视图最顶端的窗口显示和行为操作,PhoneWindow的setContentView方法最终会生成一个DecorView对象,DecorView是PhoneWindow的内部类,继承自FrameLayout,所以调用Activity方法的setContentView方法最终会生成一个FrameLayout类型的DecorView组件,该组件将作为整个应用程序的顶层视图。

在DecorView中添加根布局,根布局中包含一个id为content的FrameLayout内容布局,LayoutInflater将Activity的XML内容布局解析成View树形结构,最后添加到id为content的FrameLayout内容布局中,具体关系可以参考下图


ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。


View的绘制流程是从ViewRoot的performTraversals方法开始的,经过measure、layout、draw三个过程将一个View绘制出来。measure用来测量View的宽和高,layout用来确定View在父容器中的放置位置,draw负责将View绘制在屏幕上。


performTraversals会依次调用performMeasure、performLayout、performDraw三个方法,这三个方法分别完成View的measure、layout、draw三大流程,在performMeasure中调用measure方法,在measure方法中又调用onMeasure方法,在onMeasure方法中会对所有子元素进行measure过程。这样measure流程就从父容器传到子元素了,接着子元素继续重复父容器的measure过程。如此就完成了整个View树的遍历。performLayout和performDraw过程与performMeasure都是类似的。


Measure过程决定了View的宽高,measure完成以后可以通过getMeasureWidth和getMeasureHeight获取到View测量后的宽高。几乎所有情况下它都等于实际View的宽高。


Layout过程决定了View的四个顶点坐标和实际View的宽高,完成后通过getTop,getBottom,getLeft,getRight拿到View的四个顶点的位置,getWidth和getHeight拿到View的最终宽高。


DecorView内部一般包含两部分,标题栏和内容栏,Activity通过setContentView所设置的布局文件就是被加到内容栏中的,而内容栏的id是content,所以我们可以通过ViewGroupviewGroup = findViewById(android.R.id.content) 得到content,通过content.getChildAt(0)得到我们设置的View。


2.    MeasureSpec

MeasureSpec很大程度决定了View的尺寸规格,在测量过程中,系

统会将View的LayoutParams根据父容器施加的规则转换成MeasureSpec,再根据MeasureSpec测量出View的宽高。

MeasureSpec代表一个32位的int值,分为SpecMode(高2位,测量规格)和SpecSize(低30位,规格大小)。

       SpecMode分三类,

       UNSPECIFIED : 父容器不对View有任何限制,要多大给多大

       EXACTLY : 父容器已经测量出View所需要的精确大小,View的最终大小就是specSize所指定的值。对应于LayoutParams中的match_parent和具体的数值。

       AT_MOST : 父容器给定了一个可用大小,View的大小不能大于这个值。对应于LayoutParams中的wrap_content。

        MeasureSpec和LayoutParams的关系

对于DecorView,它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定。对于普通的View,由父容器的MeasureSpec和自身的LayoutParams决定。

        当View采用固定宽高时,不论父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小是LayoutParams中的大小

        当View的宽高时match_parent时,如果父容器是EXACTLY模式,那么View也是EXACTLY模式,并且大小是父容器剩余的空间。如果父容器是AT_MOST模式,那么View也是AT_MOST模式,并且大小不会超过父容器的剩余空间。

        当View的宽高是wrap_content时,不论父容器是EXACTLY模式还是AT_MOST模式,View都是AT_MOST模式,并且大小不会超过父容器的剩余空间。


3.    View的工作流程

Measure过程

如果只是一个原始的View,通过measure方法就可以完成测量,如果是一个ViewGroup除了测量自己还要遍历所有子View的measure方法,并且子View还会递归执行这个过程。

View的measure过程由measure方法来完成,而measure是一个 final方法,子类不可以重写,而measure方法会调用onMeasure方法。


上述方法给View设置了测量方法的测量值,这个值是通过getDefaultSize方法获得的,我们接这个看这个方法。


这个方法里的size是通过getSuggestedMinimumHeight和getSuggestedMinimumWidth获得,这两个方法的原理都一样。


如果View没有设置背景,那么View的高度就是mMinHeight,这个值是由android:minHeight这个属性设置。

如果View有设置背景,那么View的高度就是mMinHeight和背景最小高度之间的较大者。

那么这个背景最小高度又是啥呢,看下源码


返回的是Drawable的原始高度

我们继续看getDefaultSize方法,根据measureSpec得到specMode,如果是UNSPECIFIED模式,就返回getSuggestedMinimumHeight方法的值,如果是EXACTLYAT_MOST返回measureSpec中的specSize。

从getDefaultSize的实现来看,View的宽高由specSize决定,所以直接继承自View的自定义控件需要重写onMeasure方法并设置wrap_content的自身大小,否则在布局中使用wrap_content相当于match_parent。因为这种情况的specMode是AT_MOST模式,宽高是specSize,而specSize又是parentSize,也就是父容器可使用的空间大小,所以View的宽高就等于父容器当前剩余空间的大小,也就相当于在布局文件中使用match_parent。那怎么解决这个问题呢


ViewGroup的measure过程

       ViewGroup并没有重写View的onMeasure方法,提供了一个measureChildren方法


ViewGroup在进行measure时对每一个子元素进行测量, 通过measureChild这个方法


这个方法取出子元素的LayoutParams,通过getChildMeasureSpec创建子元素的MeasureSpec,然后传递给子元素的measure方法进行测量。

LinearLayout#onMeasure

在LinearLayout的onMeasure方法中,系统会遍历每个子元素,并对每个子元素执行measureChildBeforeLayout,这个方法会调用子元素的measure方法,子元素进入measure过程。并且系统会通过mTotalLength来存储LinearLayout在竖直方向的初步高度。每测量一个子元素mTotalLength就会增加,增加的主要是子元素的高度以及在竖直方向的margin。子元素测量完毕后,LinearLayout会测量自己的大小。对于竖直的LinearLayout,在水平方向的测量遵循View的测量过程,其布局如果是match_parent或者具体的数值,就与View的过程一致,如果是wrap_content,那么它的高度就是所有子元素占用高度的总和。

View的measure过程完成以后通过getMeasureWidth和getMeasureHeight方法就能获取到View的测量宽高。但有时View可能要多次measure才能确定最终的测量宽高。所以一般在onLayout方法中获取View的最终测量宽高。

获取View测量宽高的时机。

1.    在Activity或View中重写onWindowFocusChanged,在该方法中获取View测量宽高。需要注意的是,Activity在获得焦点或失去焦点时都会调用这个方法。所以在重写时需要加一个是否获得焦点的判断。

2.    view.post() 把一个Runnable对象添加到消息队列中,等Looper调用时测量,在run方法里测量。

3.    使用ViewTreeObserver接口,当View树状态发生改变或可见性改变时,接口方法会被回调。所以在第一次调用完后就移除监听。保证以后不会再触发。

4.    通过view.measure(),需要View的LayoutParams

match_parent : 无法测量,因为此时View的宽高是parentSize,而此时并不知道parentSize。

wrap_content : 因为specSize是30位,所以就用View能支持的最大值去构造measureSpec。


具体数值xxx px


Layout过程

       Layout的作用是ViewGroup来确定子元素的位置,当ViewGroup位置确定后,它在onLayout方法中会遍历所有子元素并调用layout方法,在layout方法中onLayout方法又会被调用。


Layout方法首先会通过setFrame方法设置View四个顶点的位置,接着调用onLayout方法,确定View的位置,View和ViewGroup中并没有实现这个方法,而是在具体的控件中实现的。具体看下LinearLayout的onLayout


这个方法根据调用了layoutVertical和layoutHorizontal,这两个方法是类似的。layoutVertical方法会遍历所有的子元素并调用setChildFrame方法为子元素指定位置,childTop会逐渐变大,这也符合竖直方向LinearLayout的性质,而setChildFrame就是调用子元素的layout方法,确定自己的位置。这样一层一层就完成了整个View树的layout过程。

       之前说过一个问题,View的测量宽高和实际的宽高有区别,因为在View的默认实现中,View的测量宽高和实际宽高是一样的,但是测量宽高形成于measure时期,而最终宽高形成于layout时期,两者的赋值时机不同,在layout方法中可以对测量宽高做一些改变,这就使最终宽高不同于测量宽高,虽然这样没有任何意义。另外measure过程会有多次,其测量值可能与最终宽高不同。但最终还是相同的。

       draw过程就是将View绘制到屏幕上面,有以下几步

a.     绘制背景(background.draw)

b.    绘制自己(onDraw)

c.     绘制children(dispatchDraw)

d.    绘制装饰(onDrawScrollBars)

这些方法从draw源码就能看出来


View的绘制传递是通过dispatchDraw完成的,dispatchDraw会遍历所有子元素的draw方法,如此draw事件就一层层传递下去,到这View的工作原理就分析完了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值