Android之View的工作原理
Android中的View在Android的知识体系中扮演着重要的角色。
简单来说,View就是Android在视觉的体现。我们所展现的页面就是Android提供的GUI库中控件的组合。但是当要求不能满足于控件的时候,我们就需要自定义控件/自定义View来满足我们的要求。
为了掌握自定义View,我们需要了解View的底层工作原理,了解View的测量流程,布局流程以及绘制流程,还有View常见的回调方法,比如构造方法,onAttach,onVisibilityChanged,onDetach等,以及滑动效果处理。
View的加载流程:
1.1 View 随着 Activity 的创建而加载,startActivity 启动一个 Activity 时,在 ActivityThread
的 handleLaunchActivity()
会执行 Activity 的 onCreate
方法,这个时候会调用 setContentView
加载布局,
创建出 DecorView
并将我们的layout加载到 DecorView
中。
1.2 当执行到 handleResumeActivity
时,Activity 的 onResume
方法被调用, 然后 WindowManager
会将 DecorView
设置给 ViewRootImpl
,
这样,DecorView
就被加载到Window中了,此时界面还没有显示出来, 还需要经过 View的 measure,layout 和 draw 方法,才能完成 View 的工作流程。
View的绘制 是 由ViewRoot
来负责的,每一个DecorView
都有一个与之关联的ViewRoot
, 这种关联关系是由WindowManager
维护的,
1.3 将DecorView
和 ViewRoot
关联之后,
ViewRootImpl
的 requestLayout
会被调用 以完成初步布局,通过scheduleTraversals
方法向主线程发送消息请求遍历,
最终调用ViewRootImpl
的 performTraversals
方法, 这个方法会执行 View 的 measure layout 和 draw 流程。
PhoneWindow以及 其内部类DecorView 都是在执行了Activity.setContentView之后,就已经进行了初始化。
注意:虽然PhoneWindow和DecorView都已经进行了初始化,但是并没有将DecorView加入到WindowManager中。
直到执行了ActivityThread.handleResunmeActivity()--WindowManager.addView()(会new ViewRootImpl()),
此时创建才将DecorView与WindowManager进行了绑定。
1. View的绘制流程
View的绘制流程主要就是measure,layout,draw这三个流程,对应的View的方法是onMeasure(),onLayout(),onDraw()这三个方法,即测量,布局,绘制流程。
其中measure 是测量View的宽高,layout确定View的布局内容,设置四个顶点,长宽,最后通过draw将View的内容绘制到屏幕上。
在View绘制前,我们了解下Activity中的onCreate的中执行setContentView()方法之后View是显示在屏幕上的,这个过程就不分析了,辅助我们理解整个流程。
当调用Activity的setContentView()方法之后,会调用PhoneView类的 setContentView方法(PhoneView类是抽象类Window的实现类,Window 类用来描述 Activity 视图最顶端的窗口显示和行为操作), PhoneView类的setContentView() 最终会生成DecorView对象(DecorView是PhoneView类的内部类)。
下面是DecorView的机构图可以参考一下,需要进入到View层,就需要先进入到DecorView层。
ViewRoot 对应的实现类是 ViewRootImpl 类,他是连接 WindowManager 和DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。
在 ActivityThread 中,当 activity对象被创建完毕后, 会将DecorView 添加到Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联, 这个过程参见源码。
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparms, panelParentView);
View的绘制流程就是从 ViewRoot的performTraversals方法开始的, 它经过measuer,layout和draw三个过程才能最终将一个View绘制出来,针对performTraversals的大致流程。如下图所示:
如图所示,performTraversals会一次调用performMeasure,performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure,layout和draw这三个流程。其中performMeasure会调用measure方法,measure方法又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这时候measure流程就从父元素传递到了子元素,这样就完成了一次measure的过程。接着子元素会重复父元素measure的过程,如此反复就完成了父元素View的遍历。
同理performLayout和perforDraw也是同样的道理。唯一不同的是,performDraw的传递过程是draw方法中的dispatchDraw方法来实现,本质上原理都是一致的。
meaure过程决定了view的宽高。我们可以通过getMeasureWidth和getMeasureHeight方法来获取View的宽高。几乎在所有的情况下,这个宽高就是View的最终宽高。layout过程决定了view的四个点的坐标和实际的宽高。可以通过getTop,getRight, getLeft,getBottom来得到四个点的坐标,通过getWidth和getHeight得到实际的宽高。draw的过程就是显示的过程,只有在draw完成之后才能最终显示在屏幕上。
DecorView 其实是一个 FrameLayout,View层事件都先经过DecorView ,然后才传给View。
2. 理解MeasureSpec
MeasureSpec,测量规格,测量说明,从名字上看起来都知道它的作用就是决定View的测量过程或者说它在很大程度上决定了View的尺寸规格。除此之外还有父容器也会影响View的创建过程。在测量过程中,系统会将View的LayoutParams根据父容器的规则转换成对应的MeasureSpex,然后再根据这个MeasureSpec来测量View的宽高。
MeasureSpec代表了一个32位的int的值,高2位代表了SpecMode,低30位代表了SpecSize。SpecMode指测量模式,SpecSize是指在某种测量模式下的规格大小。MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的内存分配,为了方便操作,其提供了打包和解包方法源码如下:
//将 SpecMode 和 SpecSize 打包,获取 MeasureSpec
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//将 MeasureSpec 解包获取 SpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//将 MeasureSpec 解包获取 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 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。
3. 理解MeasureSpec 和 LayoutParams 的对应关系
对于DecorView,它的 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来决定;对于普通 View,它的MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定,MeasureSpec一旦确定,onMeasure中就可以确定View的宽高。
对普通的 View 来说,View的 measure过程是由其ViewGroup传递而来的,这里先看一下 ViewGroup 的 measureChildWithMargins 方法:
* @param child 要被测量的 View
* @param parentWidthMeasureSpec 父容器的 WidthMeasureSpec
* @param widthUsed 父容器水平方向已经被占用的空间,比如被父容器的其他子 view 所占用的空间
* @param parentHeightMeasureSpec 父容器的 HeightMeasureSpec
* @param heightUsed 父容器竖直已经被占用的空间,比如被父容器的其他子 view 所占用的空间
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//第一步,获取子 View 的 LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//第二步,获取子 view 的 WidthMeasureSpec,其中传入的几个参数说明:
//parentWidthMeasureSpec 父容器的 WidthMeasureSpec
//mPaddingLeft + mPaddingRight view 本身的 Padding 值,即内边距值
//lp.leftMargin + lp.rightMargin view 本身的 Margin 值,即外边距值
//widthUsed 父容器已经被占用空间值
// lp.width view 本身期望的宽度 with 值
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//获取子 view 的 HeightMeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
// 第三步,根据获取的子 veiw 的 WidthMeasureSpec 和 HeightMeasureSpec
//对子 view 进行测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上述方法会对子元素进行measure,在调用子元素的measure方法之前就会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。
从代码上来看,子 view 的 MeasureSpec 的创建与父容器的 MeasureSpec 、子 view 本身的 LayoutParams 有关,此外还与 view 本身的 margin 和 padding 值有关,具体看一下ViewGroup的 getChildMeasureSpec 方法:
/*
* @param spec 父容器的 MeasureSpec,是对子 View 的约束条件
* @param padding 当前 View 的 padding、margins 和父容器已经被占用空间值
* @param childDimension View 期望大小值,即layout文件里设置的大小:可以是MATCH_PARENT,
*WRAP_CONTENT或者具体大小, 代码中分别对三种做不同的处理
* @return 返回 View 的 MeasureSpec 值
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 获取父容器的 specMode,父容器的测量模式影响子 View 的测量模式
int specMode = MeasureSpec.getMode(spec);
// 获取父容器的 specSize 尺寸,这个尺寸是父容器用来约束子 View 大小的
int specSize = MeasureSpec.getSize(spec);
// 父容器尺寸减掉已经被用掉的尺寸
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 如果父容器是 EXACTLY 精准测量模式
case MeasureSpec.EXACTLY:
//如果子 View 期望尺寸为大于 0 的固定值,对应着 xml 文件中给定了 View 的具体尺寸大小
//如 android:layout_width="100dp"
if (childDimension >= 0) {
//那么子 View 尺寸为期望值固定尺寸,测量模式为精准测量模式 EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//如果子 View 期望尺寸为 MATCH_PARENT 填充父布局
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 那么子 View 尺寸为 size 最大值,即父容器剩余空间尺寸,为精准测量模式 EXACTLY
//即子 View 填的是 Match_parent, 那么父 View 就给子 View 自己的size(去掉padding),
//即剩余全部未占用的尺寸, 然后告诉子 View 这是 Exactly 精准的大小,你就按照这个大小来设定自己的尺寸
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
//如果子 View 期望尺寸为 WRAP_CONTENT ,包裹内容
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子 View 尺寸为 size 最大值,即父容器剩余空间尺寸 ,测量模式为 AT_MOST 最大测量模式
//即子 View 填的是 wrap_Content,那么父 View 就告诉子 View 自己的size(去掉padding),
//即剩余全部未占用的尺寸,然后告诉子 View, 你最大的尺寸就这么多,不能超过这个值,
//具体大小,你自己根据自身情况决定最终大小。一般当我们继承 View 基类进行自定义 View 的时候
//需要在这种情况下计算给定 View 一个尺寸,否则当使用自定义的 View 的时候,使用
// android:layout_width="wrap_content" 属性就会失效
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器为 AT_MOST 最大测量模式
case MeasureSpec.AT_MOST:
// 子 View 期望尺寸为一个大于 0的具体值,对应着 xml 文件中给定了 View 的具体尺寸大小
//如 android:layout_width="100dp"
if (childDimension >= 0) {
//那么子 View 尺寸为期望固定值尺寸,为精准测量模式 EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//如果子 View 期望尺寸为 MATCH_PARENT 最大测量模式
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子 View 尺寸为 size,测量模式为 AT_MOST 最大测量模式
//即如果子 View 是 Match_parent,那么父 View 就会告诉子 View,
//你的尺寸最大为 size 这么大(父容器尺寸减掉已经被用掉的尺寸,即父容器剩余未占用尺寸),
//你最多有父 View的 size 这么大,不能超过这个尺寸,至于具体多大,你自己根据自身情况决定。
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//同上
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器为 UNSPECIFIED 模式
case MeasureSpec.UNSPECIFIED:
// 子 View 期望尺寸为一个大于 0的具体值
if (childDimension >= 0) {
//那么子 View 尺寸为期望值固定尺寸,为精准测量模式 EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//如果子 View 期望尺寸为 MATCH_PARENT 最大测量模式
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子 View 尺寸为 0,测量模式为 UNSPECIFIED
// 父容器不对 View 有任何的限制,要多大给多大
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
//如果子 View 期望尺寸为 WRAP_CONTENT ,包裹内容
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子 View 尺寸为 0,测量模式为 UNSPECIFIED
// 父容器不对 View 有任何的限制,要多大给多大
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述方法的主要作用就是根据父容器的MesaureSpec,同时结合View本身的LayoutParams来确定子元素的Measure,参数中的padding是指父容器中的已经占用的控件大小,子元素的可用的控件的大小为父容器的尺寸减去padding。
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0,specSize- padding);
getChildMeasureSpec() 清楚展示了普通View的MeasureSpec的创建规则。根据父容器的 MeasureSpec 和 view 本身的 LayoutParams 来确定子元素的 MeasureSpec 的整个过程,这个过程清楚的展示了普通 view 的 MeasureSpec 的创建规则,整理一下(图片来源艺术探索)。
只要提供了父容器的MeasureSpec和子元素的LayoutParams,就可以快速地确定子元素的MeasureSpec,有了MeasureSpec就可以进一步确定子元素测量后的大小。
总结:
-
当View是固定的宽高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式,并且大小是LayoutParams 中的大小。
-
当View的宽高是match_parent的时候,如果父容器的模式是精确模式,那么 View 也是精确模式,并且大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式,并且大小是不会超过父容器的剩余空间。
-
当 View 的宽高是 wrap_content 时,不管父容器的模式是精确模式还是最大模式,View 的模式总是最大模式,并且大小不超过父容器的剩余空间。
4. View 的measure过程
View 的工作流程主要是指 measure、layout、draw 这三大流程,即测量、布局和绘制,其中 measure 确定 View 的测量宽和高,layout 确定 View 的最终宽和高及 View 的四个顶点位置,而 draw 是将 View 绘制到屏幕上。
4.1 measure过程
measure过程要分情况,如果只是一个原始的View,那么通过measure方法就完成了测量过程,如果是ViewGroup,那么就需要首先测量自己的过程,然后再遍历调用子元素的measure方法,各个子元素在地柜去执行这个流程,下面是对这两种情况的分别讨论。
4.1.1 View的Measure过程
View 的 measure 过程由 measure 方法来完成, measure 方法是一个 final 类型,子类不可以重写,而 View 的 measure() 方法中会调用 onMeasure 方法,因此我们只需要分析 onMeasure 方法即可,源码如下:
/**
* @param widthMeasureSpec 父容器所施加的水平方向约束条件
* @param heightMeasureSpec 父容器所施加的竖直方向约束条件
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//设置 view 高宽的测量值
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上面方法很简单,setMeasuredDimension方法就是给 View 设置了测量高宽的测量值,而这个测量值是通过 getDefaultSize 方法获取,那么接着分析 getDefaultSize 方法:
/**
* @param size view 的默认尺寸,一般表示设置了android:minHeight属性
* 或者该View背景图片的大小值
* @param measureSpec 父容器的约束条件 measureSpec
* @return 返回 view 的测量尺寸
*/
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:
//如果 测量模式为 UNSPECIFIED ,表示对父容器对子 view 没有限制,那么 view 的测量尺寸为
//默认尺寸 size
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
//如果测量模式为 AT_MOST 最大测量模式或者 EXACTLY 精准测量模式,
//那么 View 的测量尺寸为 MeasureSpec 的 specSize
//即父容器给定尺寸(父容器当前剩余全部空间大小)。
result = specSize;
break;
}
return result;
}
getDefaultSize方法的逻辑很简单,如果 测量模式为 UNSPECIFIED ,表示对父容器对子 view 没有限制,那么 view 的测量尺寸为默认尺寸 size。如果测量模式为 AT_MOST 最大测量模式或者 EXACTLY 精准测量模式,那么 View 的测量尺寸为 MeasureSpec 的 specSize,即父容器给定尺寸(父容器当前剩余全部空间大小)。
这里来分析一下 UNSPECIFIED 条件下 View 的测量高宽默认值 size 是通过 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 函数获取,这两个方法原理一样,这里我们就看一下 getSuggestedMinimumHeight() 源码:
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground.getMinimumWidth());
}
从getSuggestedMinimumHeight代码可以看出,如果 View 没有背景,View 的高度就是 mMinHeight,这个 mMinHeight 是由 android:minHeight 这个属性控制,可以为 默认为0,如果有背景,就返回 mMinHeight 和背景的最小高度两者中的最大值;同理getSuggestedMinimumWidth也是一样。
public int getMinimumWidth(){
final int intrinsicWidth = getInstrinsicWidth();
return intrinsicWidth >0?intrinsicWidth : 0;
}
getMinimumWidth方法返回的就是Drawable的原始宽度,前提是这个Drawable的原始宽度,否则就返回0。
从 getDefaultSize 方法可以看出,View 的高/宽是由 父容器传递进来的 specSize 决定,因此可以得出结论: 直接继承自 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时候的自身大小,而设置的具体值需要根据实际情况自己去计算或者直接给定一个默认固定值,否则在布局中使用 wrap_content 时候就相当于使用 match_parent ,因为在布局中使用 wrap_content 的时候,它的 specMode 是 AT_MOST 最大测量模式,在这种模式下 View 的宽/高等于 speceSize 大小,即父容器中可使用的大小,也就是父容器当前剩余全部空间大小,这种情况,很显然,View 的宽/高就是等于父容器剩余空间的大小,填充父布局,这种效果和布局中使用 match_parent 一样,解决这个问题代码如下:
@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);
// 在 MeasureSpec.AT_MOST 模式下,给定一个默认值
//其他情况下沿用系统测量规则即可
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
上面代码中在 widthSpecMode 或 heightSpecMode 为 MeasureSpec.AT_MOST 我们就给定一个对应的 mWith 和 mHeight 默认固定值宽高,而这个默认值没有固定依据,需要我们根据自定义的 view 的具体情况去计算给定。
4.1.2 ViewGroup 的 measure 过程
ViewGroup 除了完成自己的测量过程还会遍历调用所有子 View 的measure方法,而且各个子 View 还会递归执行这个过程,我们知道 View Group 继承自 View ,是一个抽象类,因此没有重写 View onMeasure 方法,也就是没有提供具体如何测量自己的方法,但是它提供了一个 measureChildren 方法,定义了如何测量子 View 的规则,代码如下:
/**
* @param widthMeasureSpec 该 ViewGroup 水平方向约束条件
* @param heightMeasureSpec 该 ViewGroup 竖直方向约束条件
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
//逐一遍历获取得到 ViewGroup 中的子 View
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//对获取到的 子 view 进行测量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
我们再看一下对子 View 进行测量的 measureChild 方法 :
/**
* @param child 要进行测量的子 view
* @param parentWidthMeasureSpec ViewGroup 对要进行测量的子 view 水平方向约束条件
* @param parentHeightMeasureSpec ViewGroup 对要进行测量的子 view 竖直方向约束条件
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//第一步,获取 View 的 LayoutParams
final LayoutParams lp = child.getLayoutParams();
//第二步,获取 view 的 WidthMeasureSpec,其中传入的几个参数说明:
//parentWidthMeasureSpec 父容器的 WidthMeasureSpec
//mPaddingLeft + mPaddingRight view 本身的 Padding 值,即内边距值
// lp.width view 本身期望的宽度 with 值
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
//同上
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 第三步,根据获取的子 veiw 的 WidthMeasureSpec 和 HeightMeasureSpec
//调用子 view 的 measure 方法,对子 view 进行测量,具体后面的测量逻辑就是和我们前面分析
// view 的测量过程一样了。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChild的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着讲MeasureSpec直接传递给View的measure方法来进行测量。
ViewGroup 并没有定义具体的测量过程,这是因为 ViewGroup 是一个抽象类,其不同子类具有不同的特性,导致他们的测量过程有所不同,不能有一个统一的 onMeasure 方法,所以其测量过程的 onMeasure 方法需要子类去具体实现,比如 LinearLayout 和 RelativeLayout 等,下面通过 LinearLayout 的 onMeasure 方法来分析一下 ViewGroup 的测量过程。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
//垂直方向的 LinearLayout 测量方式
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
//水平方向的 LinearLayout 测量方式
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
上面代码可以看出 ViewGroup 内部测量方式分为垂直方向和水平方向,两者原理基本一样,下面看一下垂直方向的 LinearLayout 测量方式,由于这个方法代码比较长,所以贴出重点部分:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
......................
//记录总高度
float totalWeight = 0;
final int count = getVirtualChildCount();
//获取测量模式
final int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
...........
//第1步,对 LinearLayout 中的子 view 进行第一次测量
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
//获取子 view 的 LayoutParams 参数
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
//第1.1步,满足该条件,第一次测量时不需要测量该子 view
if (heightMode == View.MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// 满足该条件的话,不需要现在计算该子视图的高度。
//因为 LinearLayout 的高度测量规格为 EXACTLY ,说明高度 LinearLayout 是固定的,
//不依赖子视图的高度计算自己的高度
//lp.height == 0 && lp.weight > 0 说明子 view 使用了权重模式,即希望使用 LinearLayout 的剩余空间
// 测量工作会在之后进行
//相反,如果测量规格为 AT_MOST 或者 UNSPECIFIED ,LinearLayout
// 只能根据子视图的高度来确定自己的高度,就必须对所有的子视图进行测量。
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
//标记未进行测量
skippedMeasure = true;
} else {
// else 语句内部是对子 view 进行第一次测量
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// 如果 LiniearLayout 不是 EXACTLY 模式,高度没给定,
//说明 LiniearLayout 高度需要根据子视图来测量,
// 而此时子 view 模式为 lp.height == 0 && lp.weight > 0 ,是希望使用 LinearLayout 的剩余空间
// 这种情况下,无法得出子 view 高度,而为了测量子视图的高度,
//设置子视图 LayoutParams.height 为 wrap_content。
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
//该方法只是调用了 ViewGroup 的 measureChildWithMargins() 对子 view 进行测量
// measureChildWithMargins() 方法在上面 4 MeasureSpec和LayoutParams的对应关系已经分析过
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec