1、ViewRoot是什么?
ViewRoot对应于ViewRootImpl类是连接WindowManager和DecorView的纽带发起并完成View的三大流程(测量、布局、绘制)ViewRoot需要和DecorView建立联系。2、ViewRoot如何完成View的三大流程?
WechatIMG125.jpeg
ViewRoot的performTraversals()开始View的绘制流程,依次调用performMeasure()、performLayout()和performDraw()performMeasure()最终执行父容器的measure()方法,并依此执行所有子View的measure方法。performLayout()和performDraw()同理3、View三大流程的作用
measure决定了View的宽/高,测量后可以通过getMeasuredWidth/Height来获得View测量后的宽/高,除特殊情况外该值等于View最终的宽/高layout决定了View的顶点坐标以及实际View的宽/高:完成后可以通过getTop/Bottom/Left/Right获取顶点坐标,并通过getWidth/Height()获得View的最终宽/高draw决定了View的显示,最终将View显示出来 MeasuredWidth/height != getWidth/Height()的场景:更改View的布局参数并进行重新布局后,就会导致测量 != 实际值4、DecorView的作用
DecorView是顶级View,本质就是一个FrameLayout包含了两个部分,标题栏和内容栏内容栏id是content,也就是activity中setContentView所设置的部分,最终将布局添加到id为content的FrameLayout中获取content:ViewGroup content = findViewById(R.android.id.content)获取设置的View:content.getChidlAt(0)5、ViewRootIml如何和DecorView建立联系
Activity对象在ActivityThread中创建完毕后,会将DecorView添加到Window中同时会创建ViewRootImpl,调用ViewRoot的setView方法将ViewRootImpl和DevorView建立关联 root = new ViewRootImpl(view.getContext(), display) root.setView(view, wparams, panelParentView)6、ViewRoot为什么要和DecorView建立关联
DecorView等View的三大流程需要通过ViewRoot完成1、MeasureSpec是什么?
MeasureSpec是一种“测量规则”或者“测量说明书”,决定了View的测量过程View的MeasureSpec会根据自身的LayoutParamse和父容器的MeasureSpec生成。最终根据View的MeasureSpec测量出View的宽/高(测量时数据并非最终宽高)2、MeasureSpec要点解析
MeasureSpec代表一个32位int值,高2位是SpecMode,低30位是SpecSizeSpecMode是指测量模式SpecSize是指在某种测量模式下的大小类MesaureSpec提供了用于SpecMode和SpecSize打包和解包的方法3、测量模式SpecMode的类型
UNSPECIFIED:父容器不对View有任何限制,一般用于系统内部EXACTLY:精准模式,View的最终大小就是SpecSize指定的值(对应于LayoutParams的match_parent和具体的数值)AT_MOST:最大值模式,大小不能大于父容器指定的值SpecSize(对应于wrap_content)4、MeasureSpec和LayoutParams的对应关系
View的MeasureSpec是需要通过自身的LayoutParams和父容器的MeasureSpec一起才能决定DecorView(顶级View)是例外,其本身MeasureSpec由窗口尺寸和自身LayoutParams共同决定MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽/高5、普通View的Measure的创建规则
View本身布局参数为具体dp/px数值,模式:EXACTLY,尺寸:自身尺寸(不管父容器的MeasureSpec)View为match_parent, 模式:EXACTLY/AT_MOST由父容器MeasureSpec决定,尺寸:父容器目前可用大小View为wrap_content,模式:AT_MOST,尺寸:父容器可用尺寸(不能超过该尺寸)当父容器为UNSPECIFIED时,View为具体数值时规则不变;其余match_parent/wrap_content,模式均为:UNSPECIFIED,尺寸:0UNSPECIFIED一般用于系统内部多次measure的情况,不需要关注该模式。WechatIMG126.jpeg
1、Measure 过程
1、View的measure过程及要点
View的measure方法是final类型方法——表明该方法无法被重载View的measure方法会调用onMeasure方法,onMeasure会调用setMeasuredDimension方法设置View宽/高的测量值2、View的onMeasure源码要点
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //1. setMeasuredDimension方法设置View宽/高的测量值 setMeasuredDimension( //2. 第一个参数是获得的测量宽/高(通过getDefaultSize获取) getDefaultSize(getSuggestedMinimumWidth(), //3. 获取的建议最小的宽/高 widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } setMeasuredDimension方法设置View宽/高的测量值(测量值通过getDefaultSize获取)getDefaultSize用于获取View的测量宽/高3、View的getDefaultSize源码要点(决定了View宽高的测量值)
//1. 获取View宽和高的测量值 public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { //2. UNSPECIFIED模式时,宽/高为第一个参数也就是getSuggestedMinimumWidth()获取的建议最小值 case MeasureSpec.UNSPECIFIED: result = size; break; //3. AT_MOST(wrap_content)和EXACTLY(match_parent/具体值dp等)这两个模式下,View宽高的测量值为当前View的MeasureSpec(测量规格)中指定的尺寸specsize case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }4、View的getSuggestedMinimumWidth/Height()源码要点
//获取建议的最小宽度 protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } 如果View没有背景,View的最小宽度就为android:minWidth这个参数指定的值(mMinWidth),没有指定则默认为0如果View有背景,会从mMinWidth和背景的最小宽度中取最大值。背景的最小宽度(getMinimumWidth())本质就是Drawable的原始宽度(ShapeDrawable无原始宽度,BitmapDrawable有原始宽度——图片的尺寸)5、View的wrap_content和match_parent效果一致的原因分析
根据View的onMeasure方法中的getDefaultSize方法,我们可以发现在两种模式下,View的测量值等于该View的测量规格MeasureSpec中的尺寸。View的MeasureSpec本质是由自身的LayoutParams和父容器的MeasureSpec决定的。当View为wrap_content时,该View的模式为AT_MOST,且尺寸specSize为父容器的剩余空间大小。当View为match_parent时,该View的模式跟随父容器的模式(AT_MOST/EXACTLY), 且尺寸specSize为父容器的剩余空间大小。因此getDefaultSize中无论View是哪种模式,最终测量宽/高均等于尺寸specSize,因此两种属性效果是完全一样的(View的大小充满了父容器的剩余空间)除非给定View固定的宽/高,View的specSize才会等于该固定值。6、自定义View需要重写onMeasure方法,并写明两种模式的处理方法
//1. 重写onMeasure,特殊处理wrap_content的情况 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){ //2. 均为wrap_content时, 将值设置为android:minWidth/Height属性指定的值 setMeasuredDimension(mWidth, mHeight); }else if(widthSpecMode == MeasureSpec.AT_MOST){ //3. 哪个为wrap_content哪个就用android:minXXX属性给定的最小值 setMeasuredDimension(mWidth, heightSpecSize); }else if(heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize, mHeight); } }7、ViewGroup(抽象类)的measure流程
ViewGroup没有onMeasure方法,只定义了measureChildren方法(onMeasure根据不同布局难以统一)measureChildren中遍历所有子元素并调用measureChild方法measureChild方法中会获取子View的MeasureSpec,然后调用子元素View的measure方法进行测量8、getChildMeasureSpec获取子元素MeasureSpec的要点
子View的MeasureSpec是根据自身的LayoutParams和父容器SpecMode生成当子View的布局参数为wrap_content,且父容器模式为AT_MOST时,效果与子元素布局为match_parent是一样的。因此当子View的布局参数为wrap_content时,需要指定默认的宽/高9、LinearLayout的onMeasure()分析
ViewGroup因为布局的不同,无法统一onMeasure方法,具体内容根据布局的不同而不同,这里直接以LinearLayout进行分析onMeasure会根据orientation选择measureVertical或者measureHorizontal进行测量measureVertical本质是遍历子元素,并执行子元素的measure方法,并获得子元素的总高度以及子元素在竖直方向上的margin等。最终LinearLayout会测量自己的大小,在orientation的方向上,如果布局是match_parent或者具体数值,测量过程与View一致(高度为specSize);如果布局是wrap_content,高度是所有子元素高度总和,且不会超过父容器的剩余空间,最终高度需要考虑在竖直方向上的padding10、如何获取View的测量宽/高
在measure完成后,可以通过getMeasuredWidth/Height()方法,就能获得View的测量宽高在一定极端情况下,系统需要多次measure,因此得到的值可能不准确,最好的办法是在onLayout方法中获得测量宽/高或者最终宽/高11、如何在Activity启动时获得View的宽/高
Activity的生命周期与View的measure不是同步运行,因此在onCreate/onStart/onResume均无法正确得到若在View没有测量好时,去获得宽高,会导致最终结果为0有四种办法去正确获得宽高A. onWindowFocusChanged获得View的宽/高
//1. View已经初始化完毕,可以获得宽高 @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); //2. Activity得到焦点和失去焦点均会调用一次(频繁onResume和onPause会导致频繁调用) if(hasFocus){ int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }B. view.post(runnable)获得View的宽/高
//1. 通过post将一个runnable投递到消息队列尾部 view.post(new Runnable() { @Override //2. 等到Looper调用次runnable时,View已经完成初始化 public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } });C. ViewTreeObserver获得View的宽/高(Kotlin版)
val observer = imageView.viewTreeObserver //1. 使用ViewTreeObserver的接口,可以再View树状态改变或者View树内部View的可见性改变时,onGlobalLayout会被毁掉 observer.addOnGlobalLayoutListener(object :ViewTreeObserver.OnGlobalLayoutListener { //2. 能正确获取View宽/高 override fun onGlobalLayout() { //3. 随着View树状态改变,会多次调用。因此需要移除监听器 imageView.viewTreeObserver.removeGlobalOnLayoutListener(this) val width = imageView.measuredWidth val height = imageView.measuredHeight } })D. view.measure()获得View的宽/高(Kotlin)
mathc_parent的情况下是不可以的,因为需要知道parent的size,这里无法获取。具体数值 //1. 具体数值时(dp/px),让View重新测量 val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) imageView.measure(widthMeasureSpec, heightMeasureSpec) //2. 完成后就可以获得宽/高 val width = imageView.width val height = imageView.heightwrap_content
//1. wrap_content,将specSize设置为30位二进制的最大值 (1 << 30) - 1,让View重新测量(在AT_MOST情况下是合理的) val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 shl 30) - 1, View.MeasureSpec.AT_MOST) val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 shl 30) - 1, View.MeasureSpec.AT_MOST) imageView.measure(widthMeasureSpec, heightMeasureSpec) //2. 完成后就可以获得宽/高 val width = imageView.width val height = imageView.height1、View的layout过程
使用layout方法确定View本身的位置layout中调用onLayout方法确定所有子View的位置2、View的layout()源码分析
调用setFrame()设置View四个定点位置(即初始化mLeft,mRight,mTop,mBottom的值)之后调用onLayout确定子View位置,该方法类似于onMeasure,View和ViewGroup中均没有实现,具体实现与具体布局有关。3、LinearLayout的onLayout方法
根据orientation选择调用layoutVertical或者layoutHorizontallayoutVertical中会遍历所有子元素并调用setChildFrame(里面直接调用子元素的layout方法)层层传递下去完成了整个View树的layout过程setChildFrame中的宽/高实际就是子元素的测量宽/高(getMeasure…后直接传入)4、View的测量宽高和最终宽高有什么区别?
等价于getMeasuredWidth和getWidth有什么区别getWidth = mRight - mLeft,结合源码测量值和最终值是完全相等的。区别在于:测量宽高形成于measure过程,最终宽高形成于layout过程(赋值时机不同)也有可能导致两者不一致:强行重写View的layout方法,在传参方面改变最终宽/高(虽然这样毫无实际意义)某些情况下,View需要多次measure才能确定自己的测量宽高,在前几次测量中等到的值可能有最终宽高不一致。但是最终结果上,测量宽高=最终宽高PS :有兴趣的加入Android工程师交流QQ群:752016839 主要针对Android开发人员提升自己,突破瓶颈,相信你来学习,会有提升和收获。
1、draw的步骤
绘制背景(drawBackground(canvas))绘制自己(onDraw)绘制children(dispatchDraw)-遍历调用所有子View的draw方法绘制装饰(如onDrawScollBars)2、View特殊方法setWillNotDraw
若一个View不绘制任何内容,需要将该标志置为true,系统会进行相应优化默认View不开启该标志位默认ViewGroup开启该标志位如果我们自定义控件继承自ViewGroup并且本身不进行绘制时,就可以开启该标志位当该ViewGroup明确通过onDraw绘制内容时,就需要显式关闭WILL_NOT_DRAW标志位。
