学习内容
View的底层工作原理,比如View的测量流程、布局流程以及绘制流程;以及常见的View回调方法;熟悉掌握前面的知识后,自定义View的时候也会更加的得心应手。
4.1 初识ViewRoot和DecorView
- ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
- View的绘制流程从ViewRoot的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过程,如此反复完成了整个View数的遍历。另外两个过程类似,大致调用流程如下图:
- measure过程决定了View的宽/高,完成后可通过getMeasuredWidth/getMeasureHeight方法来获取View测量后的宽/高。Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过getTop、getBotton、getLeft和getRight拿到View的四个定点坐标。Draw过程决定了View的显示,完成后View的内容才能呈现到屏幕上。
- 如下图,DecorView作为顶级View,一般情况下它内部包含了一个竖直方向的LinearLayout,里面分为两个部分(具体情况和Android版本和主题有关),上面是标题栏,下面是内容栏。在Activity通过setContextView所设置的布局文件其实就是被加载到内容栏之中的。
//获取内容栏
ViewGroup content = findViewById(R.android.id.content); //获取我们设置的Viewcontext.getChildAt(0);
DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后才传给我们的View。
4.2 理解MeasureSpec
- MeasureSpec很大程度上决定一个View的尺寸规格,测量过程中,系统会将View的layoutParams根据父容器所施加的规则转换成对应的MeasureSpec,再根据这个measureSpec来测量出View的宽/高。
- MeasureSpec代表一个32位的int值,高2位为SpecMode,低30位为SpecSize,SpecMode是指测量模式,SpecSize是指在某种测量模式下的规格大小。
MpecMode有三类;
1.UNSPECIFIED 父容器不对View进行任何限制,要多大给多大,一般用于系统内部
2.EXACTLY 父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的match_parent和具体数值这两种模式。
3.AT_MOST 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,不同View实现不同,对应LayoutParams中的wrap_content。 - 当View采用固定宽/高的时候,不管父容器的MeasureSpec的是什么,View的MeasureSpec都是精确模式兵其大小遵循Layoutparams的大小。 当View的宽/高是match_parent时,如果他的父容器的模式是精确模式,那View也是精确模式并且大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且起大小不会超过父容器的剩余空间。 当View的宽/高是wrap_content时,不管父容器的模式是精确还是最大化,View的模式总是最大化并且不能超过父容器的剩余空间。
4.3 View的工作流程
1. View的measure过程
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
- setMeasuredDimension方法会设置View的宽/高的测量值
- getDefaultSize方法返回的大小就是measureSpec中的specSize,也就是View测量后的大小,绝大部分情况和View的最终大小(layout阶段确定)相同。
- getSuggestedMinimumWidth方法,作为getDefaultSize的第一个参数(建议宽度)
- 直接继承View的自定义控件,需要重写onMeasure方法并且设置
wrap_content时的自身大小,否则在布局中使用了wrap_content相当于使用了match_parent。解决方法:在onMeasure时,给View指定一个内部宽/高,并在wrap_content时设置即可,其他情况沿用系统的测量值即可。
2. ViewGroup的measure过程
- 对于ViewGroup来说,除了完成自己的measure过程之外,还会遍历去调用所有子元素的measure方法,个个子元素再递归去执行这个过程,和View不同的是,ViewGroup是一个抽象类,没有重写View的onMeasure方法,提供了measureChildren方法。
- measureChildren方法,遍历获取子元素,子元素调用measureChild方法
- measureChild方法,取出子元素的LayoutParams,再通过getChildMeasureSpec方法来创建子元素的MeasureSpec,接着将MeasureSpec传递给View的measure方法进行测量。
- ViewGroup没有定义其测量的具体过程,因为不同的ViewGroup子类有不同的布局特征,所以其测量过程的onMeasure方法需要各个子类去具体实现。
- measure完成之后,通过getMeasureWidth/Height方法就可以获取View的测量宽/高,需要注意的是,在某些极端情况下,系统可能要多次measure才能确定最终的测量宽/高,比较好的习惯是在onLayout方法中去获取测量宽/高或者最终宽/高。
如何在Activity中获取View的宽/高信息
因为View的measure过程和Activity的生命周期不是同步进行,如果View还没有测量完毕,那么获取到的宽/高就是0;所以在Activity的onCreate、onStart、onResume中均无法正确的获取到View的宽/高信息。下面给出4种解决方法。
- Activity/View#onWindowFocusChanged。
onWindowFocusChanged这个方法的含义是:VieW已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用。 - view.post(runnable)。
通过post将一个runnable投递到消息队列的尾部,当Looper调用此runnable的时候,View也初始化好了。 - ViewTreeObserver。
使用ViewTreeObserver的众多回调可以完成这个功能,比如OnGlobalLayoutListener这个接口,当View树的状态发送改变或View树内部的View的可见性发生改变时,onGlobalLayout方法会被回调。需要注意的是,伴随着View树状态的改变,onGlobalLayout会被回调多次。 - view.measure(int widthMeasureSpec,int heightMeasureSpec)。
(1). match_parent:
无法measure出具体的宽高,因为不知道父容器的剩余空间,无法测量出View的大小
(2). 具体的数值(dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); view.measure(widthMeasureSpec,heightMeasureSpec);
(3). wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); view.measure(widthMeasureSpec,heightMeasureSpec);
2. View的layout过程
- 在View的默认实现中,View的测量宽/高和最终宽/高是相等的,测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程。
3. View的draw过程
- 将View绘制到屏幕上,大概的几个步骤:
1.绘制背景background.draw(canvas)
2.绘制自己(onDraw)
3.绘制children(dispatchDraw)
4.绘制装饰(onDrawScrollBars)
- View的绘制过程是通过dispatchDraw来实现的,它会遍历所有子元素的draw方法。
- 如果一个View不需要绘制任何内容,那么设置setWillNotDraw为true后,系统会进行相应的优化;ViewGroup默认为true,如果我们的自定义ViewGroup需要通过onDraw来绘制内容的时候,需要显示的关闭它。
4.4 自定义View
- 直接继承View或ViewGroup的控件, 需要在onmeasure中对wrap_content做特殊处理。
- 直接继承View的控件,如果不在draw方法中处理padding,那么padding属性就无法起作用。直接继承ViewGroup的控件也需要在onMeasure和onLayout中考虑padding和子元素margin的影响,不然padding和子元素的margin无效。
- View内部提供了post系列的方法,完全可以替代Handler的作用。
- View中有线程和动画,需要在View的onDetachedFromWindow中停止。
- 自定义View示例请看原著和随书源码
本篇文章主要介绍以下几个知识点:
- 初识 ViewRoot 和 DecorView;
- 理解 MeasureSpec;
- View 的工作流程:measure、layout、draw。
4.1 初识 ViewRoot 和 DecorView
为更好的理解 View 的三大流程(measure
、layout
、draw
),先了解一些基本的概念。
ViewRoot 对应于 ViewRootImpl
类,是连接 WindowManager
和 DecorView
的纽带,View 的三大流程都是通过 ViewRoot
来完成的。
View 的绘制流程从 ViewRoot 的 performTraversals
方法开始,它经过 measure
(测量 View 的宽高),layout
(确定 View 在父容器的位置) 和 draw
(负责将 View 绘制在屏幕上) 三个过程才能将一个 View 绘制出来,如下:
DecorView 是一个 FrameLayout
,View 层的事件都先经过 DecorView
,再传递给 View。
DecorView 作为顶级 View,一般它内部会包含一个竖直方向的 LinearLayout,上面是标题栏,下面是内容栏。在 Activity 中通过 setContentView
设置的布局文件就是被加到内容栏中,而内容栏的 id 为 content,可通过 ViewGroup content = findviewbyid(android.R.id.content)
得到 content,通过 content.getChildAt(0)
得到设置的 View。其结构如下:
4.2 理解 MeasureSpec
MeasureSpec 很大程度上决定了一个 View 的尺寸规格。在 View 的测量过程中,系统会将 View 的 LayoutParams
根据父容器所施加的规则转换成对应的 MeasureSpec
,再根据这个 measureSpec
来测量出 View 的宽高(测量宽高不一定等于 View 的最终宽高)。
4.2.1 MeasureSpec
MeasureSpec 代表一个32位 int 值,高两位代表 SpecMode(测量模式),低30位代表 SpecSize(某个测量模式下的规格大小),MeasureSpec 内部的一些常量定义如下:
private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; // MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配 public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } // 解包:获取其原始的 SpecMode @MeasureSpecMode public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } // 解包:获取其原始的 SpecSize public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }
SpecMode 有三类,其含义分别如下:
-
UNSPECIFIED
父容器不对 View 有任何的限制(一般用于系统内部),表示一种测量的状态 -
EXACTLY
父容器检测出 View 的精度大小,此时 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的match_parent
和具体的数值这两种模式 -
AT_MOST
父容器指定一个可用大小即SpecSize,View 的大小不能大于这个值。它对应于 LayoutParams 中的wrap_content
4.2.2 MeasureSpec 和 LayoutParams 的对应关系
Layoutparams 需要和父容器一起才能决定 View 的 MeasureSpec,一旦确定 MeasureSpec 后,onMeasure 中就可以确定 View 的测量宽高。
顶级 View(DecorView),其 MeasureSpec 由窗口的尺寸和自身的 Layoutparams 来共同决定;普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 Layoutparams 来决定。
对于 DecorView,在 ViewRootImpl
中的 measureHierarchy
方法中的一段代码展示了其 MeasureSpec 的创建过程:
// 其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth , lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
接下来看下 getRootMeasureSpec
方法的实现:
private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
上述代码明确了 DecorView 的 MesourSpec 的产生过程,根据其 Layoutparams 的宽高的参数来划分,遵守如下规则:
-
LayoutParams.MATCH_PARENT:
精确模式,大小就是窗口的大小 -
LayoutParams.WRAP_CONTENT:
最大模式,大小不定,但是不能超出屏幕的大小 -
固定大小(比如100dp):
精确模式,大小为 LayoutParams 中指定的大小
对于 普通的 View,指布局中的 View,其 measure 过程由 ViewGroup 传递而来,先看下 ViewGroup 的 measureChildWithMargins
方法:
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); // 调用子元素的 measure 方法前会通过上面的 getChildMeasureSpec 方法得到子元素的 MesureSpec child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
上述对子元素进行 measure,显然,子元素的 MesureSpec 的创建和父容器的 MesureSpec 、子元素的 LayoutParams 有关和 View 的 margin 有关,其中 getChildMeasureSpec
方法如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); // 参数中的 pading 是指父容器中已占有的控件大小 // 因此子元素可以用的大小为父容器的尺寸减去 pading int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
上述方法主要作用是根据父容器的 MeasureSpec 同时结合 View 本身的 Layoutparams 来确定子元素的 MesureSpec。
上面getChildMeasureSpec
展示了普通 View 的 MeasureSpec 创建规则,也可参考下表(表中的 parentSize 指父容器中目前可使用的大小):
当 View 采用固定宽/高时,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式并且其大小遵循 LayoutParams 中的大小。
当 View 的宽/高是 match_parent
时,若父容器是精准模式,那么 View 也是精准模式并且其大小是父容器的剩余空间;若父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。
当 View 的宽/高是 wrap_content
时,不管父容器的模式是精准还是最大化,View 的模式总是最大化,并且大小不能超过父容器的剩余空间。
注:UNSPECIFIED 模式主要用于系统内部多次 Measure 的情形,一般不需关注此模式。
综上,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地确定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以进一步确定出子元素测量后的大小了。
4.3 View 的工作流程
View 的工作流程主要是指 measure
(测量,确定 View 的测量宽/高)、layout
(布局,确定 View 的最终宽/高和四个顶点的位置)、draw
(绘制,将 View 绘制到屏幕上)这三大流程。
4.3.1 measure 过程
若只是一个原始的 View,那么通过 measure 方法就完成了其测量过程,若是一个 ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程。
4.3.1.1 View 的 measure 过程
View 的 measure 过程由其 measure 方法来完成,measure 方法中会去调用 View 的 onMesure
方法如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置 View 宽/高的测量值 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
其中 getDefaultSize
方法如下:
public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
上面的 AT_MOST 和 EXACTLY 这两种情况,可理解为 getDefaultSize
返回的大小就是 mesourSpec 中的 specSize,而这个 specSize 就是 View 测量后的大小(测量大小不一定等于 View 的最终大小)。
至于 UNSPECIFIED 这种情况,一般用于系统内部的测量过程,View 的大小为 getDefaultSize
的第一个参数是 size,其宽/高获取方法如下:
protected int getSuggestedMinimumWidth() { // 1. 若 View 没有设置背景,View 的宽度为 mMinwidth, // 而 mMinwidth 对应于 android:minwidth 这个属性所指定的值, // 因此 View 的宽度即为 android:minwidth 属性所指定的值, // 若这个属性不指定,那么 mMinWidth 则默认为0; // 2. 若 View 指定了背景,则View的宽度为max(mMinwidth,mbackground().getMininumwidth) return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }
上面注释分析了 getSuggestedMinimumWidth
方法的实现,getSuggestedMinimumHeight
和它的原理一样。注释中未说明的 mBackground.getMinimumWidth()
方法(即 Drawable 的 getMinimumWidth
方法)如下:
public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); // 返回 Drawable的原始宽度(有原始宽度的话),否则就返回0 return intrinsicWidth > 0 ? intrinsicWidth : 0; }
总结 getSuggestedMinimumWidth
的逻辑:
若 View 没设背景,那么返回 android:minwidth
所指定的值(可为0);
若 View 设了背景,则返回 android:minwidth
和背景的最小宽度这两者中的最大值。
View 在 UNSPECIFIED 情况下的测量宽/高即为 getSuggestedMinimumWidth
和getSuggestedMinimumHeight
的返回值 。
结论:直接继承 View 的自定义控件需要重写 onMeasure
方法并设置 wrap_content
时的自身大小,否则在布局中使用 wrap_content
就相当于使用 match_parent
。
从上述代码中知道,若 View 在布局中使用 wrap_content
,那么它的 specMode 是 AT_MOST 模式,它的宽/高等于 specSize;此情况下 View 的 specSize 是 parentSize,而 parentSize 是父容器中目前可以使用的大小,即父容器当前剩余的空间大小。显然,View 的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用 match_parent
完全一致。
解决上述问题代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec); // 给 View 指定一个默认的内部宽/高(mWidth, mHeight),并在 wrap_content 时设置此宽/高即可 // 对于非 wrap_content 情形,沿用系统的测量值即可 //(注:TextView、ImageView 等针对 wrap_content 情形,它们的 onMeasure 方法做了特殊处理) 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); } }
4.3.1.2 ViewGroup 的 measure 过程
和 View 不同的是,ViewGroup 是一个抽象类,它没有重写 View 的 onMeasure
方法,但它提供了一个 measureChildren
方法:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; // ViewGroup 在 measure 时,会对每一个子元素进行 measure for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
上述代码中的 measureChild
方法如下:
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { // 1. 取出子元素的 LayoutParams final LayoutParams lp = child.getLayoutParams(); // 2. 通过 getChidMeasureSpec 来创建子元素的 MeasureSpec final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); // 3. 将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
上面代码注释说明了 measurechild
的思想。
由于 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现;不同的 ViewGroup 子类有不同的布局特性,它们的测量细节各不相同,如 LinearLayout 和 RelativeLayout 这两者的布局特性不同,因此 ViewGroup 无法对其 onMeasure 方法做统一实现。
下面通过 LinearLayout 的 onMeasure 方法来分析 ViewGroup 的 measure 过程,先来看一下 LinearLayout 的 onMeasure 方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }
这里选择查看竖直方向的 LinearLayout 测量过程,即 measureVertical
方法(其源码比较长就不贴了),这里只描述其大概逻辑:系统会遍历子元素并对每个子元素执行 measureChildBeforeLayout
方法,此方法内部会调用子元素的 measure 方法,当子元素测量完毕之后,LinearLayout 会根据子元素的情况来测量自己的大小。
View 的 measure 过程完成后,通过 getMeasureWidth/Height
可以正确地获取到 View 的测量宽/高。但在系统要多次 measure 才能确定最终的测量宽/高的情况下,在 onMeasure 方法中拿到的测量宽/高可能是不准确的。因此建议在 onLayout 方法中去获取 View 的测量宽/高或者最终宽/高。
问题:如何在 Activity 已启动的时候获取某个 View 的宽/高?
注:由于 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,无法保证 Activiy 执行了 onCreate、onStart、onResume
时某个 View 已经测量完毕了,从而在 onCreate、onStart、onResume
中均无法正确得View的宽/高信息(若 View 还没测量完毕,那么获得的宽/高就是0)。
这里给出四种方法:
(1)Activity/View#onWindowFocusChanged
onWindowFocusChanged
方法是指:View 已初始化完毕,宽/高已准备好,此时去获取宽/高是没问题的(注:当 Activity 继续执行和暂停执行时,onWindowFocusChanged
均会被调用,若频繁地进行 onResume
和 onPause
,那么 onWindowFocusChanged
也会被频繁地调用)。典型代码如下:
public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (!hasFocus) { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }
(2)view.post(runnable)
通过 post 可将一个 runnable 投递到消息队列的尾部,然后等待 Lopper 调用此 runnable 时,View 就初始化好了。典型代码如下:
protected void onStart() { super.onStart(); view.post(new Runnable() { @Override public void run() { int width = mTextView.getMeasuredWidth(); int height = mTextView.getMeasuredHeight(); } }); }
(3)ViewTreeObserver
使用 ViewTreeObserver
的众多回调可完成这个功能,典型代码如下:
protected void onStart() { super.onStart(); ViewTreeObserver observer = view.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); int width = mTextView.getMeasuredWidth(); int height = mTextView.getMeasuredHeight(); } }); }
(4)view.measure(int widthMeasureSpec , int heightMeasureSpec)
通过手动测量 View 的宽高,此方法较复杂,根据 View 的LayoutParams 来分情况来处理:
-
match_parent:无法测量出具体的宽高
-
具体的数值(dp/px):如宽高都是100dp,如下 measure:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY); int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY); view.measure(widthMeasureSpec, heightMeasureSpec);
- wrap_content:如下measure:
// View 的尺寸使用30位的二进制表示,即最大是30个1(即 2^30-1),也就是 (1<<30)-1
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST); int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST); view.measure(widthMeasureSpec, heightMeasureSpec);
关于 View 的 measure,网络上有两个错误的用法。为什么说是错误的,首先其违背了系统的内部实现规范(因为无法通过错误的 MeasureSpec 去得出合理的 SpecMode,从而导致 measure 过程出错),其次不能保证 measure 出正确的结果。
- 第一种错误的方法:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED); int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED); view.measure(widthMeasureSpec, heightMeasureSpec);
- 第二种错误的方法:
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
4.3.2 layout 过程
Layout 是 ViewGroup 用来确定子元素的位置的,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调其 layout 方法,在 layout 方法中 onLayout 又被调用。layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置,View 的 layout 方法如下:
public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // 1. 通过 setFrame 方法来设定 View 的四个顶点的位置, // 即初始化 mLeft,mTop,mRight,mBottom 这四个值 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // 2. View 的四个顶点一旦确定,那么 View 在父容器的位置也就确定了, // 接下来会调用onLayout方法(用途:父容器确定子元素的位置) onLayout(changed, l, t, r, b); mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }
和 onMeasure 类似,onLayout 的具体位置实现同样和具体布局有关,所有 View 和 ViewGroup 均没有真正的实现 onLayout 方法。 LinearLayout 的 onLayout 如下:
protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { layoutVertical(l, t, r, b); } else { layoutHorizontal(l, t, r, b); } }
LinearLayout 的 onLayout 和 onMeasure 的实现逻辑类似,就 layoutVertical 来说,其主要代码如下:
void layoutVertical(int left, int top, int right, int bottom) { . . . final int count = getVirtualChildCount(); // 遍历所有子元素 for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); . . . if (hasDividerBeforeChildAt(i)) { childTop += mDividerHeight; } childTop += lp.topMargin; // 调用 setChildFrame 为子元素指定对应的位置 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } }
上述方法中的 setChildFrame
方法,仅仅是调用子元素的 layout 方法而已,如下:
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }
这样父元素在 layout 方法中完成自己的定位后,就通过 onLayout 方法去调用子元素的 layout 方法,子元素又会通过自己的 layout 方法来确定自己的位置,这样一层一层传递下去完成整个 View 树的 layout 过程。
问题:View 的测量宽/高和最终宽/高有什么区别?(即:View 的 getMeasureWidth
和getWidth
这两个方法有什么区别?)
为了回答这个问题,先看下 getWidth
和 getHeight
方法的实现:
public final int getWidth() { return mRight - mLeft; } public final int getHeight() { return mBottom - mTop; }
可以看出,getWidth
、getHeight
返回的刚好是 View 的测量宽度、高度。
对于上面的问题:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,一个是 layout 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。
日常开发中可用认为 View 的测量宽/高 = 最终宽/高,但某些特殊情况下,如重写 View 的 layout 方法如下:
public void layout(int l,int t,int r, int b){ super.layout(l, t, r + 100, b + 100); }
上述代码会导致在任何情况下 View 的最终宽/高总是比测量宽/高大 100px。
4.3.3 draw 过程
Draw 过程其作用是将 View 绘制到屏幕上面。View 的绘制过程遵循如下几步:
(1)绘制背景 background.draw(canvas)
(2)绘制自己 (onDraw)
(3)绘制 children (dispatchDraw)
(4)绘制装饰 (onDrawSrcollBars)
这一点通过 draw 方法的源码可看出来:
public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // we're done... return; } . . . }
View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历所有子元素的 draw 方法,如此 draw 事件就一层层地传递下去。View 有一个特殊的方法 setwilINotDraw
:
public void setwilINotDraw(boolean willNotDraw){ // 若一个 View 不需要绘制任何内容,那么设置这个标记位为 true 以后,系统会进行相应的优化。 // 默认情况下,View 没有启用这个校化标记位,但 ViewGroup 会默认启用这个优化标记位。 setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); }
实际开发中,自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。若明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时,需要显式地关闭 WILL_NOT_DRAW
这个标记位。