view的工作原理
Posted gitzzp
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了view的工作原理相关的知识,希望对你有一定的参考价值。
view的工作原理
基本概念
ViewRoot
对应ViewRootImpl类 是连接WindowManager和DecorView的纽带,view的三大流程均通过ViewRoot来完成,在ActivityThread中,当Activity对象呗创建完毕后,会将DectorView添加到window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DectorView对象进行关联,参照如下源码root = new ViewRootImpl(view.getContext(),display); root.setView(view,wparams,panelParentView());
View的绘制流程是从view的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过程 接着子元素同样会重复这个过程 如此反复直到完成整个viewTree的遍历
同理 performLayout和performDraw的传递流程也是类似的 唯一的不同是performDraw的传递过程是在draw方法中调用dispatchDraw来实现的 本质上并没有区别
measure过程决定了view的宽高,measure完成之后, 可以通过getMeasureWidth和getMeasureHeight方法来获得view测量之后的宽高,几乎所有的情况下都是正确的,但是有例外,后边会详细说明,layout过程决定了view四个顶点的坐标和view实际的宽高,完成之后,可以通过getTop、getBottom、getLeft和getRight来拿到view四个顶点的位置,并可以通过getWidth和getHeight来拿到view最终的宽高,draw过程则决定了view的显示 只有draw方法完成之后view的内容才能呈现在屏幕上
DectorView
作为顶级view 一般情况下来说他的内部会包含一个竖直方向的LinearLayout,在这个LinearLayout中分为上下两个部分,上边是标题栏,下边是内容栏,在Activity中我们通过setContentView所设置的布局文件就是被添加到内容栏中的 由此可以解释,为什么在activity中指定布局的方法不叫setView而是叫setContentView ,我们添加的布局被添加到id为content的FrameLayout中去了,可以通过如下代码来获得content和我们所设置的布局ViewGrounp content = (ViewGrounp)findViewById(android.R.id.content); content.getChildAt(0);
MeasureSpec
译为测量规格,MeasureSpec代表了一个32位的int值,高2位代表SpecMode,低30位代表了SpecSize,SpecMode表示测量模式,SpecSize表示在某种测量模式之下的规格大小,MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的内存占用 为了方便操作 系统提供了打包和解包方法SpecMode有三个类 如下所示
UNSPECIFIED 未指明的 父容器不对View有任何限制 要多大给多大 一般多用于系统内部 表示一种测量状态 不需过多纠结 EXACTLY 确切的 父容器已经检测出View所需的精确大小 这个时候view的最终大小就是SpecSize所指定的值 对应于LayoutParams中的match_parent和具体数值两种模式 AT_MOST 最大范围之内 父容器指定了一个可用大小 即SpecSize view的大小不能超过这个值 对应于LayoutParams中的wrap_content
MeasureSpec和LayoutParams的对应关系
对于等级view(DectorView)和普通view来说 MeasureSpec的转换过程略有不同,对于DectorView 其MeasureSpec,由窗口的尺寸和其自身的LayoutParams决定,对于普通view,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定view的测量宽高
DectorView的MeasureSpec的产生遵循如下规则
LayoutParams.MATCH_PARENT:精确模式 大小就是窗口的大小
LayoutParams.WRAP_CONTENT:最大模式 大小不定 但是不能超过窗口的大小
固定大小(如:100dp) 精确模式 大小为LayoutParams中指定的大小对于普通view来说 由于他的MeasureSpec受父容器的MeasureSpec影响 所以情况要比较复杂 源码内容也比较多,所以我们借助一个表格来表示其规则(其中parentSize是指父容器中目前可以使用的空间)
parentSpecMode
childLayoutParamsEXACTLY AT_MOST UNSPECIFIED dp/px EXACTLY
childSizeEXACTLY
childSizeEXACTLY
childSizematch_parent EXACTLY
parentSizeAT_MOST
parentSizeUNSPECIFIED
0wrap_content AT_MOST
parentSizeAT_MOST
parentSizeUNSPECIFIED
0总结一下:
1.当view使用固定宽高的时候,不管父容器的SpecMode是什么,view的MeasureSpec都是精确模式并且其大小遵循自身的LayoutParams中的大小
2.当view使用match_parent时,如果其父容器是精确模式,那么view也会是精确模式,并且使用父容器中可用的宽高,如果父容器是最大模式,那么view也将是最大模式,并且使用父容器中剩余的宽高
3.当view使用wrap_content时,不管父容器是什么模式,view的MeasureSpec都是最大模式,并且其大小会使用父容器剩余可用的大小注:最后一个多用于系统内部 可以忽略不做过多关注
View的工作流程
view的工作流程主要包括measure layout draw三大流程,即测量、布局和绘制,其中measure确定view的测量宽高,layout确定view的最终宽高和四个顶点的实际位置 ,draw则将view绘制到屏幕上
measure过程
measure过程是三大流程中比较复杂的一个,要分情况来看,如果只是一个view,那么measure过程就是完成了其测量过程,如果是一个ViewGrounp,除了要完成自己的测量过程之外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程 直到测量完成view的measure过程
view的measure过程是尤其measure方法来完成的,measure方法是一个final方法,无法重写,在view的measure方法中又会调用view的onMeasure方法,通过阅读view的onMeasure方法可知,view的宽高由specSize决定,由此我们可以得出以下结论:
直接继承view的自定义控件需要重写onMeasure方法,并设置wrap_content时的自身大小,否则在局中使用wrap_content就相当于使用match_parent
为什么呢?如果我们在view中使用wrap_content时,那么view的specMode是AT_MOST,在这种模式之下他的宽高等于specSize,而我们在前边阅读源码的过程中,总结过一个表格,查表格可知,这种情况下,不管父容器是那种SpecMode,view的SpecSize始终都会是parentSize,也就是说,会占用父容器剩余的所有空间,这种效果和使用match_parent有什么区别呢?
查看系统的TextView和ImageView等控件的源码可知 他们针对这种情况也做出了对应的处理 在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){ setMeasuredDimension(mWidth,mHeight); }else if(widthSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(mWith,heightSpecSize); }else if(heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,meight); } }
在上边代码中 我们只需要给view指定一个默认的内部宽高(mWidth和mHeight)并在使用wrap_content时对view设置这个宽高即可
ViewGrounp的measure过程
对于ViewGrounp来说,除了完成自己的measure过程之外,还需要遍历子元素,调用所有子元素的measure方法,各个子元素再递归去指向这个过程,直到measure完成,和view不同的是,ViewGrounp是一个抽象类,因此他并没有重写view的onMeasure方法,而是提供了一个叫measureChildren的方法,来遍历各个子元素,调用子类的measure方法,我们直到 ViewGrounp并没有定义其测量的具体过程,因为ViewGrounp是一个抽象类,其测量过程中的onMeasure方法需要各个子类去自己实现 因为各个ViewGrounp有截然不同的特性,所以ViewGrounp没办法像view意向对onMeasure方法进行统一,只能由子类根据自身特性来去实现如何在Activity中去获取某个view的测量宽高
在Activity中我们无法在onCreate onStart onResume等生命周期中去获取某一个view的测量宽高,因为View的measure过程和Activity的生命周期方法并不是同步的,因此无法保证Activity执行了这些生命周期方法时某个view一定已经测量完毕了,如果view此时还没有测量完毕,那么我们所获得的宽高就是0,所以我们需要另外的方法,来获取view的measure宽高Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,这时候去获取某个view的测量宽高是没有问题的,但是需要注意的是,onWindowFocusChanged方法在,Activity的窗口得到和失去焦点的时候均会被调用一次,也就是说,如果频繁的调用onPause和onResume方法的话,该方法会频繁的被调用,典型代码如下:public void onWindowFocusChanged(boolean hasFocus){ super.onWindowFocusChanged(hasFocus); if(hasFocus){ int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }
view.post(runnable)
通过post可以将一个runnable投递到view消息队列的尾部 然后等待Looper调用次runnable时,view的其他任务已经完成 view已经初始化好了 典型代码如下protected void onStart(){ super.onStart(); view.post(new Runnable(){ @override public void run(){ int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener
这个接口,当ViewTree的状态发生改变或者viewTree内部的View的可见性发生改变的时候
onGlobalLayoutListener方法会被回调,因此我们可以在这个时候获取view的measure宽高
需要注意的是,伴随着viewTree的状态改变 该方法会被回调多次 典型代码如下:protected void onStart(){ super.onStart(); ViewTreeObserver observer = view.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){ @suppressWarnings("deprecation") @override public void onGlobalLayout(){ view.getViewTreeObserver.removeGlobalLayoutListener(this); int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
view.measure(int widthMeasureSpec,int hegihtMeasureSpec)
通过手动的对view进行measure来得到view的measure宽高 这种方法比较复杂 需要分情况具体处理,根据view的LayoutParams来分 而且容易出现错误用法 不推荐使用
layout过程
layout的作用是ViewGrounp用来确定子元素的位置,当viewGrounp的位置被确定之后 他在onLayout中会遍历所有的子元素,并调用子元素的layout方法,子元素确定自身位置之后,又会调用子元素的onLayout方法来确定其子元素的位置,以此类推,直到viewTree绘制完毕,layout方法确定view本身的位置 onLayout方法确定view子元素的位置layout方法的大致流程如下,首先会通过setFrame方法来设定view的四个顶点的位置,即初始化mLeft mTop mBottom mRight这四个值,view的四个顶点一旦被确定,view在父容器中的位置也就确定了,接着会调用onLayout方法,这个方法的用途是确定子元素的位置,在onLayout方法中,会调用setChildFrame,在该方法中又会调用子元素的layout方法,这样父元素在layout方法中确定自身的位置之后,就通过onLayout方法去调用子元素的layout方法,子元素又会通过layout来确定自己的位置,层层传递,直到ViewTree绘制完毕
Draw过程
阅读源码可知 View的绘制过程遵循以下几个步骤:- 绘制背景 background.draw(canvas)
- 绘制自身 onDraw(canvas)
- 绘制children dispatchDraw(canvas)
- 绘制装饰 onDrawScrollBars(canvas)
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有元素的draw方法 如此draw,事件就层层传递下去,直到绘制完毕.
自定义View
自定义View的分类
继承View重写onDraw方法
主要用于实现一些不规则的效果,采用这种方法需要注意的就是需要自己支持wrap_content和padding,如果不定义wrap_content 在布局文件中使用该效果,回合match_parent一样(具体原因前边已做分析) 同时 view的padding属性需要在onDraw方法中去实现,否则没有效果继承ViewGrounp派生出特殊的layout
主要用于实现自定义布局,需要注意合适的处理viewGrounp的测量,布局过程,并且同时处理子元素的测量和布局.继承特定的view(如TextView等)
主要用于对某种已有的view的功能进行拓展,该方法比较容易实现,不需要自己去处理wrap_content和padding继承特定的ViewGrounp(如LinearLayout等)
与2类似,同时又不需要自己处理ViewGrounp的测量和绘制过程
自定义View需要注意的问题
让View支持wrap_content
直接继承view或者viewGrounp的控件,需要在onMeasure中对wrap_content做特殊处理如果有必要 让自己的view支持padding
直接继承view的控件,如果不在draw方法中处理padding,那么padding属性将失效,直接继承ViewGrounp的控件,需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响 不然将导致padding和子元素的margin属性失效尽量不要在view中使用Handler
view内部本身提供了post系列方法 完全可以替代Hnadler的作用View中如果有线程和动画需要停止 参照View#onDetachedFromWindow,当包含这个View的Activity退出或者当前view被remove时,View的onDetachedFromWindow方法会被调用,我们可以在这个回调中停止线程或者动画,与此对应的是onAttachedToWindow方法,当包含这个view的Activity启动时,View的onAttchedToWindow方法会被调用,同时,当View不可见是我们也需要停止线程和动画
View带有滑动嵌套时 需要处理好滑动冲突
具体请参见博客:view基础知识介绍(二)
参考资料:Android开发艺术探索
以上是关于view的工作原理的主要内容,如果未能解决你的问题,请参考以下文章
深入理解View知识系列二- View底层工作原理以及View的绘制流程