前言
探究分析了View绘制的总体流程:onMeasure、onLayout、onDraw三大方法。
知识准备
ViewRoot
ViewRoot对应ViewRootImpl类,是连接WindowManager与DecorView的纽带。View的三大流程都是通过ViewRoot完成的。ActivityThread中,Activity对象被回收时,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView对象建立关联。
代码示例:
View绘制流程:从ViewRoot的performTraversals开始,经过measure、layout、draw三大流程后才将View绘制出来。
performTraversals方法(8W多行代码),会依次调用performMeasure、performLayout、performDraw方法(这三个方法分别完成顶级View - DecorView 的measure、layout、draw方法)
其中performMeasure方法会调用measure方法,在measure方法中又会调用onMeasure方法,onMeasure方法则会对所有子元素进行measure,从而达到measure流程从父容器传递到子元素中的目的。接着子元素会重复父容器的measure过程,如此反复完成整个View树的遍历。
最后,perfromLayout、performDraw的传递过程也是类似的,唯一不同,而performDraw的传递过程在draw方法中通过dispatchDraw来实现。
DecorView - 继承FrameLayout
DecorView作为顶级View,一般情况下会包含一个LinearLayout,该LinearLayout分为上下两部分,titlebar部分、android.R.id.content部分(所以,setContentView方法其实就是将布局添加到id为content的FrameLayout中)正如小标题,DecorView本质是FrameLayout,View层时间都必先经过DecorView然后在传递给其中的View。
MeasureSpec - 很大程度决定View的尺寸规格
MeasureSpec代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)
小白科普:Java中int为4个字节,Android使用第一个高位字节存储Mode,剩下三个字节存储Size
MeasureSpec内部实现原理
小白科普:<< 是移位运算,3<<30表示的是首先把3变成二进制的11然后右边补30个0所组成的一个二进制的数。
MearsureSpec和LayoutParams对应关系
系统内部是通过MeasureSpec进行View测量的,但正常情况下,都是使View指定MeasureSpec。View测量时候系统会将LayoutParams在父容器约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。因此,Measure需要由LayoutParams和父容器一起决定。
另外,对于顶级View(DecorView),其MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定。Measure一旦决定后,onMeasure中in个即可获得View的测量宽高。
|
|
对于普通View(布局中的View),View的measure方法需要由ViewGroup传递过来
再看看ViewGroup中的measureChildWithMargins方法
上述方法,对子元素进行measure,调哟in个子元素measure之前会获取子元素的MeasureSpec。显然,子元素MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,还与View的margin以及padding有关(具体需要研究ViewGroup的getChildMeasureSpec方法)。
|
|
上述方法,主要根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec。
另外注意,子元素可用大小为父容器尺寸减去padding。
View工作流程
measure 确定View宽高
layout确定View最终宽高和四个顶点位置
draw将View绘制到屏幕
View的生命周期与工作流程
View生命周期示意图
View工作流程示意图
探究Measure过程
两种情况,若只是一个原始的View,通过measure方法就完成了测量过程;如果是ViewGroup,除了完成自身的measure过程,还需要遍历子元素的measure方法,各个子元素递归去执行这个部分。(如上面的示意图所述)
View的measure方法是一个final类型的方法 - 意味着子类不能重写该方法,因此仔细研究onMeasure方法的实现效果会更好。
这里贴出View中Measure方法,有部分注释,供有兴趣的读者阅读研究。
|
|
好啦,接下来,继续研究onMeasure
|
|
setMeasuredDimension()方法会设置View宽高测量值,接下来进一步深入,研究getDefaultSize()方法。
|
|
getDefaultSize方法直接就是根据测量模式返回measureSpec中的specSize,而这个specSize就是View测量后的大小。
注意:View测量后大小 与 View最终大小 需要区分,是两个东西,因为View最终大小是在layout阶段确定的,但两者几乎所有情况都是相等的。
接下来,再继续探究getDefaultSize方法的第一个参数,从onMeasure方法中可知,该参数来源于下面两个方法
上述两个方法实现原理都一致,判断有没有背景,如果有,返回两者较大的宽高,没有则返回自己的宽高(android:minwith这个属性指定的值)。
那么,问题来了,背景最小宽高原理是什么?
上述代码中可见,Drawable的原始宽度,如果没有原始宽度,则返回0。
小白科普:ShapeDrawable无原始宽高,而BimapDrawable有原始宽高(即图片尺寸)
再谈谈ViewGroup的measure过程
主要区别:
- 除了完成自己measure过程还要遍历调用子元素的measure方法,各个子元素再递归执行该过程。
- ViewGroup是抽象类,没有重写View的onMeasure方法,而是提供了一个measureChildren的方法123456789101112131415161718192021protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {final int size = mChildrenCount;final View[] children = mChildren;for (int i = 0; i < size; ++i) {final View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {measureChild(child, widthMeasureSpec, heightMeasureSpec);}}}protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {final LayoutParams lp = child.getLayoutParams();final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}
View在measure过程中会对每一个子元素进行measure。
再细说下,measureChild方法的思路:
- 取出子元素的LayoutParams
- 通过getChildMeasureSpec离开创建子元素的MeasureSpec
- 将MeasureSpec传递给View的Measure方法进行测量。
问题:为什么ViewGroup不像View一样对其onMeasure方法做统一实现?
因为不同的ViewGroup子类会有不同的特性,因此其中的onMeasure细节不相同。
获取View宽高方法不当,可能会获取错误。
原因:View的measure过程和Activity生命周期方法执行顺序是不确定的,无法保证Activity执行了onCreate、onStart、onReasume时,View测量完毕。
如果View还没有完成测量,则获取的宽高会是0
给出四种方法解决:
- onWindowFocusChanged - 该方法被调用时候,View已经测量完毕,能够正确获取View宽高。
注意:该方法会被调用多次,Activity窗口得到焦点与失去焦点时均会被调用一次(继续执行,暂停执行)。12345678public void onWindowFocusChanged(boolean hasWindowFocus) {super.onWindowFocusChanged(hasWindowFocus);if(hasWindowFocus){//获取宽高int with = view.getMeasuredWidth();int height = view.getMeasuredHeight();}}
- view.post(runnable)
通过post将一个runnable投递到消息队列队尾,等待Looper调用此runnable时,View已初始化完毕。1234567891011protected void onStart(){super.onStart();view.post(new Runnable(){public void run(){//获取宽高int with = view.getMeasuredWidth();int height = view.getMeasuredHeight();}})}
- ViewTreeObserver
该类有众多回调接口,其中的OnGlobalLayoutListener接口,当View树状态发生变化或者View树内部的View的可见性发生改变,该方法都会被回调,利用此特性,可获得宽高。123456789101112protected void onStart(){super.onStart();viewTreeObserver observer = view.getViewTreeObserver();observer.addOnGlobalLayoutListener(new OnGlobalListener(){public void onGlobalLayout(){view.getViewTreeObserver().removeGlobalOnlayoutListener(this);//获取宽高int with = view.getMeasuredWidth();int height = view.getMeasuredHeight();}})}
- view.measure(int widthMeasureSpec,int heightMeasureSpec)
手动对View进行获取,根据View的LayoutParams不同,而采取不同手段。(因不常用,这里就不详细说明)
探究layout过程
layout主要作用是ViewGroup用来确定子元素位置(递归)。
|
|
layout方法流程:
- setFrame方法设定View四个顶点位置
- 调用onLayout方法,父容器确定子元素位置
另外,与onMeasure方法相似,onLayout方法也是各不相同的。
onLayout方法(LinearLayout、RelativeLayout等基本控件可自行尝试研究下)
探究draw过程
draw主要作用是将View绘制到屏幕上
绘制过程:
- 绘制背景(background.draw(canvas))
- 绘制自己(onDraw)
- 绘制children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
|
|
View的绘制过程传递是通过dispatchDraw来实现,dispatchDraw会遍历所有子元素的draw方法,另外,View还有一个特殊的方法setWillNotDraw
|
|
setFlags - 该方法可设置优化标记
如果View不需要绘制任何内容,那么将设置标记为true,系统会相应优化。默认情况下,View不启用这个标记位,但ViewGroup会默认启动该优化标记。
而实际开发意义:当我们自定义控件继承于ViewGroup并且本身不具备绘制功能,则开启标记。而如果明确知道一个ViewGroup需要通过onDraw来绘制内容时候,则需要显式关闭WILL_NOT_DRAW这个标记位。