文章目录
参考链接
要点提炼|开发艺术之View,基本上图都是从那里找到的,这个博主也是个大佬,建议关注。
DecorView与ViewRoot
一句话概括DecorView是最外层的ViewGroup,也可以理解为Window界面的顶级View。在事件到来时,也是先发到DecorView在传到具体的View。如图所示,他会包含一个LinearLayout,其中content就是用来装我们setContentView加载近的布局文件。
View的工作流程
measure->layout->draw
1.measure:测量view的宽高;获得了宽高,可以通过getWidth和getHeight获取。
2.layout:计算View应该放置在父View的位置;确定了View最终的宽高和四个顶点坐标。可以通过getWidth和getHeight获得最终的宽高。以及getBottom和getTop、getLeft与getRight获取四个顶点。
3.draw:将View绘制。**
具体流程
1.ViewRoot是连接WindowManger与DecorView的。ViewRoot对应ViewRootImpl类。在Activity对象创建后,会将DecorView放入Window,同时创建ViewRootImpl对象。
2.View的绘制是由ViewRoot的performtravelsals(中文为遍历的意思)方法开始的。会依次进行performmeaure,performLayout,performDraw方法。
3.首先会调用顶层View的TperformMeasure,接下来会调用measure方法,紧接着调用onMeasure方法,对全部子元素进行测量,此时就转向了子元素的measure方法,重复这个过程,一个完整的View树就测量完毕。performLayout和performDraw同理。
measureSpec
view的measure过程与measureSpec密切相关。
1.measureSpec是一个32位的,其前两位为标识位SpecMode,标识选择哪种模式,其后30位为specSize(某种测量模式下的规格大小)。
2.measureSpec共有三种模式:
1).AT_MOST(最大模式):由父View给出一个不能超过的Specsize,对应layoutparams的wrap_content
2).Excatly(确定模式):由父View给出一个确定的Specsize,对应layoutparams的match_parent或给定数值
3)UNSPECIFIED模式:父View不决定其大小,一般用于系统内部。
.
3.在父View向子View传递measure函数时,会首先计算子View的widthmeasureSpec与heightmeasureSpec,并传递。因此可知子View的MeasureSpec与父View的MeasureSpec与自身的layoutparam有关。
4.子View的MeasureSpec与父View的MeasureSpec与layoutparam的关系如下表:
1)如果layoutparams传入的是具体的数值,则无论父View是什么模式,子View都是Excatly模式,且size就是子View的长度
2)如果layoutparams传入的是matchparent,则根据父View是什么模式,子View就是什么模式,然后size是父View的长度
3)如果layoutparams传入的是wrap_content,则无论父View是什么模式,子View都是AT_MOST模式,size是父View的长度。
5.普通View的measureSpec与父View的measureSpec与LayoutParams有关,而DecorView与窗口大小与自身的LayoutParams有关。
View的工作流程
measure
View的measure
View没有子View了,直接测量自身即可。会首先调用measure方法(该方法是final类型,不可重写),紧接着调用onMeasure方法,在onMeasure方法调用setMeasureDimension,该方法会设置View的宽高。在setMeasureDimension中使用getDefaultSize方法,该方法会根据传入的measureSpec返回size值,如果measureSpec是AT_MOST或Excatly都会直接返回size。
而这里出现了一个重要问题,就是属性wrap_content不生效。可以根据之前的表格知道,子View为wrap_content时,无论父View的MeasureSpec是什么模式,都会将子View的MeasureSpec模式变成EXACTLY,同时MeasureSpec的size是父View的size。那么此时getDefalutSize就会返回父View剩余的width和height,这样就和子View为match_parent相同了。
因此我们要重写onMeasure方法,并为其设置一个宽高用于(LayoutParams)宽高为wrap_content时使用。下面为典型代码。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//分析模式,根据不同的模式来设置
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,mHeight);
}
}
ViewGroup的measure
ViewGroup除了完成自身的测量,会遍历的去调用所有子元素的的measure方法,各个子元素在递归的去做这件事。(如果子元素也是ViewGroup就会递归的继续判断,如果是View就会直接测量)。
ViewGroup是个抽象类,需要具体的子类(比如LinearLayout)来实现onMeasure。
整体流程如下,ViewGroup先调用measureChildren方法,在该方法中会遍历所有的子View,并调用measurechild方法,同时传入child,以及父MeasureSpec。measurechild方法中会根据父MeasureSpec,以及自身的Layoutparams计算自身的MeasureSpec,最后会调用子View的measure方法,并传入两个自己的MeasureSpec。
measureChildren(int widthMeasureSpec,int heightMeasureSpec){
for(child in children)
measurechild(child,widthMeasureSpec,heightMeausreSpec);
}
measurechild(View child,int parentwidthMeasureSpec,int parentheightMeausreSpec){
LayoutParams Lp = child.getLayoutParams();
int childWidthMeasurSpec= getMeasureSpec(parentwidthMeasureSpec,...);
int childHeightMeasureSpec = getMeasureSpec(parentheightMeausreSpec,..)
child.measure(childWidthMeasurSpec,childHeightMeasureSpec );
}
下图是一个ViewGroup到一个子View的测量过程。
layout方法
layout方法是计算自身的位置(确定四个顶点的位置以及View的宽高)。onLayout方法的作用是ViewGroup确定子元素的位置。
对于ViewGroup来说,会调用layout确定自身的位置,之后调用onlayout方法去遍历这些子元素并调用它们的layout方法,这些子元素会继续持续这个过程。但是ViewGroup中的onlayout方法是没有实现的,因为不同的ViewGroup需要不同的实现。
对于View来说,会调用layout方法,其中setframe方法会设置四个顶点的坐标(mLeft、mRight、mTop、mBottom),对于View来说它的onlayout是空实现。
draw方法
draw过程的入口是draw()函数,之后会进行如下几个操作。
- 绘制背景:drawbackground(canvas): 绘制本身View的背景。
- 绘制自己:onDraw(canvas) :由于不同的View会不同,因此自定义View需要重写该方法。
- 绘制children:dispatchDraw(canvas) :遍历子View,最终调用子View的draw函数重复绘制过程,在View中这里是空实现。
- 绘制装饰:onDrawScrollBars(canvas)(如 滚动指示器、滚动条、和前景等)
可以补充一点的是只有ViewGroup才有dispatchDraw方法,在View中是一个空实现,同理,只有在View中才有onDrawScrollBars方法。
其次补充一点,View有一个boolean变量WILL_NOT_DRAW用于标识不用在屏幕上绘制它。在ViewGroup中它是默认为false,在View中是true。
整体总结(复习必看)
1.DecorView是Window的顶级View,无论是事件分发还是对View进行工作流程,都是由DecorView在分给底下的View。DecorView其中是一个LinearLayout,上面是title,下面是content,布局文件就是放在这个content里的。
2.ViewRoot是连接DecorView与WindowManger的枢纽,View的measure、layout、draw都是由ViewRootImpl对象完成的。ViewRootImpl对象在Acitivity创建后,DecorView放入Window后会创建。
3.View的绘制工作是 measure->layout->draw。
measure:测量View的宽高;
layout:计算View应该放置在父View的位置
draw:将View绘制在屏幕上。
View绘制工作是以调用ViewRootImpl对象的peformTravelsals方法开始的。首先调用ViewRootImpl的performmeasure方法,进行View树的测量。进入顶级View的measure方法,之后进入onMeasure方法,顶级View会在该方法中遍历所有的子元素,并调用其Measure方法。持续上述过程直到所有的View都完成测量。之后会调用ViewRootImpl的performlayout、performdraw。
4.MeasureSpec:是一个测量标准,它是一个32位的int型变量。他的前两位是模式标记位,后30位是size。他的模式包括三类:AT_MOST、EXCATLY、UN_specfic。
其中AT_MOST是最大化模式,对应LayoutParams的wrap_content,因为这里不知道size该取什么,只知道不能大于父View所能容纳的最大值(考虑了padding在内)。
EXCATLY是确定模式,对应LayoutParams的match_parent和确定数值。确定size该取什么,取父View所能容纳的最大值,或确定的数值。
UN_SPECFIC模式:父View不影响它,size想取多少都可以,一般用于系统内部。
子View的MeasureSpec与其自身的LayoutParams有关也与父View的MeasureSpec有关。其关系如下表。
5. Measure过程:
View的measure过程如下图,首先以measure方法开始,之后调用onMeasure方法,在onMeasure方法中会调用setMeasureDimension去设置宽高;setMeasureDimension中会调用getDefaultSize方法获取宽高,当MeasureSpec为EXCATLY以及AT_MOST时都会返回size。至此View的measure结束。
ViewGroup的measure过程,其没有OnMeasure方法,会调用MeasureChildren对其子元素进行测量,具体是通过调用measurechild方法,在measurechild方法获得当前子元素的WidthMeasureSpec与HeightMeasureSpec,并传入子元素的mesure方法。子元素会重复上述操作,直至结束。
6.为什么自定义View的wrap_content不起作用?
因为当子View的LayoutParams为wrap_content时,不管父View是什么其MeasureSpec的模式都只会是AT_MOST,size为父View的size。这样在onMeasure测量时,getDefault方法就会返回这个size,这样就和match_parent相同了。
解决方法就是要重新onMeasure方法,并设置一个自定义的宽高,当LayoutParams为wrap_content时(MeasureSpec为AT_MOST),就使用setMeasureDimension传入我们自定义的宽高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//分析模式,根据不同的模式来设置
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,mHeight);
}
}
7.layout过程
ViewGroup的layout:首先layout方法用于确定自身的位置,之后使用onLayout确定子元素的位置,在onLayout中会遍历所有子元素,并调用子元素的layout方法。
View的layout:通过layout方法确定自身的位置,使用setFrame可以设置四个顶点的位置(top,bottom,left,right)。View的onlayout方法是一个空实现。
8.draw过程
draw过程一共包括以下几个方法:(1)drawBackGround:用于绘制背景(2)onDraw:用于绘制自身内容(3)dispatchDraw:用于ViewRoot去绘制子元素(4)onDrawScrollBars:用于绘制装饰
注:第四个总记不住好好记忆一下
ViewGroup的draw过程:draw()->drawBackGround()->onDraw()->dispatchDraw();其中dispatchDraw会遍历所有子元素,并调用子元素的draw方法。
View 的draw过程:draw()->drawBackGround()->onDraw()->;View的dispatchDraw是个空实现。
其次补充一点,View可以通过设置WILL_NOT_DRAW这个boolean变量来让View不绘制,在ViewGroip中该变量默认为false,View中默认为true。