Android View的工作原理 (二) View的工作流程

View的工作流程主要是指measure,layout,draw这三大流程,即测量,布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上

measure过程


measure过程主要分情况来看,如果只是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。

1 View的measure过程

View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会去调用View的onMeasure方法,因此只需要看onMeasure的实现即可。

8656692-60c99bd3be34d51e.png

上述代码很简洁,但是简洁并不代表简单,setMeasureDimension方法会设置View的宽/高的测量值,因此我们只需要看getDefaultSize这个方法即可

8656692-15836d1f54ea1d40.png

可以看出,getDefaultSize这个方法的逻辑很简单,对于我们来说,我们只需要看AT_MOST和EXACTLY这两种情况,简单的理解,其实getDefaultSize返回的大小就说measureSpec中的specSize,而这个specSize 就说View测量后的大小,这里多次提到测量后的大小,是因为View 最终的大小是在layout 阶段确定的,所以这里必须要加以区分,但几乎所有情况下View的测量大小和最终大小是相等的

至于UNSPECUFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size,即宽/高分别为getSuggestedMinimunWidth和getSuggestedMinimunHeight这两个方法的返回值

8656692-6d2b968073689667.png

这里之分析getSuggestedMinimunWidth方法的实现,getSuggestedMinimunHight和它的实现原理是一样的。从getSuggestMinimunWidth的代码可以看出,如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应于android:minWidth这个属性所指定的值,因此View的宽度即为android:minWidth属性所指定的值,这个属性如果不指定,那么mMinWidth则默认为0,如果View指定了背景,则View的宽度为max(mMinWidth,mBackgroung.getMinimunWidth())

mBackgroung.getMinimunWidth是什么呢?

8656692-24f99e41ab85b47c.png

可以看出,getMinimumWidth返回的就是Drawable的原始宽度,前提是这个Drawable的原始宽度,否则就返回0,那么Drawable在什么情况下有原始宽度呢,ShapeDrawable无原始宽/高,而BitmapDrawable 有原始宽/高

这里再总结一下getSuggestedMiniMumWidth的逻辑,如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0,如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值,getSuggestMinimunWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情况下的测量宽/高

从getDefaultSize方法的实现来看,View的宽/高由specSize决定,所以我们可以得出如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽/高等于specSize,这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小,很显然,View的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用match_parent完全一致

8656692-64b086b00071300b.png

在上面的代码中,我们只需要给View指定一个默认的内部宽/高(mWidth和mHeight),并在wrap_content时设置此宽/高即可,对于非wrap_content情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可

2 ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所以子元素的measure方法,各个子元素在递归去执行这个过程,和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫measureChildren的方法

8656692-ecb9ffa66813a676.png

从上述代码来看,ViewGroup在measure时,会对每一个子元素进行measure,measureChild这个方法的实现也很好理解

8656692-50d8b90a1916f0b1.png

measureChild的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure 方法来进行测量。

ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,比如LinearLayout,RelativeLayout等,为什么ViewGroup不像View一样对其onMeasure方法做统一的实现呢?那是因为不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,比如LinearLayout 和RelativeLayout这两者特性显然不同,因此ViewGroup无法做统一实现

View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasuredWidth/Hight方法就可以正确的获取到View的测量宽/高,在某些极端情况下,系统可能需要多次measure 才能确定最终的测量宽/高,在这种情形下,在onMeasure方法中拿到的测量宽/高可能是不准确的,一个较好的习惯是在onLayout方法中去获取View的测量宽/高。


实例

我们想在Activity已启动的时候就做了一件任务,但是这一件任务需要获取某个View的宽高,实际上在onCreate,onStart,onResume中去获取这个View的宽/高是拿不到数据的,这是因为View的measure过程和Activity的声明周期方法不是同步执行的,因此无法保证Activity 执行了onCreate,onStart,onResume时某个View已经测量完毕了,如果View还没有测量完成,那么获得的宽/高就是0,解决办法:

1 Activity/View#onWindowFoucusChanged

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

2 view.post(runnable)

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

8656692-7dd9ceffd8c526b9.png

3 View.TreeObserver

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

8656692-c737d3ac280bb77e.png

view.measure(int widthMeasureSpec,int heightMeasureSpec)

通过手动对View进行measure来得到View的宽/高,这种方式比较复杂,这里要分情况处理,根据View的LayoutParams来分

match_parent

直接放弃,无法measure出具体的宽/高,原因很简单,根据View的measure过程,构造此种MeasureSpec需要知道parentSize,根据父容量的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量出View的大小

具体的数值(dp/px)

比如宽/高都是100px,如下measure

8656692-ed65c9d60f33a143.png

wrap_content 

比如measure:

8656692-d9e5861d209e896f.png

Layout过程


Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用,layout过程和measure过程相比就简单多了,layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置

8656692-214aa52e446eb93a.png

layout方法的大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft,mRight,mTop,mBottom这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了,接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法,我们可以先看一下LinearLayout的onLayout方法

8656692-fad8e00d61a4a2f2.png

LinearLayout中onLayout的实现逻辑和onMeasure的实现逻辑类似

8656692-136d5e29b9e7c425.png

Draw过程


Draw过程比较简单,它的作用是将View绘制到屏幕上面,绘制步骤:

1 绘制背景 background.draw(canvas)

2 绘制自己(onDraw)

3 绘制children (dispatchDraw)

4 绘制装饰(onDrawScrollBars)

8656692-454e03f4477a7241.png

View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,这样draw事件就一层层地传递下去了。

View有一个特殊的方法setWillNotDraw

8656692-81e4009c2cca2922.png

如果一个View不需要绘制任何内容,那么设置这个标记位为true后,系统会进行相应的优化,默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位,当我们的自定义控件继承自ViewGroup并且自身不具备绘制功能时,就可以开启这个标记位从而使系统进行后续的优化,当知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显式的关闭WILL_NOT_DRAW这个标记位

猜你喜欢

转载自blog.csdn.net/weixin_34343000/article/details/87218691