Android中的View在Android的知识体系中扮演着重要的角色。简单来说,View就是Android在视觉的体现。我们所展现的页面就是Android提供的GUI库中控件的组合。但是当要求不能满足于控件的时候,我们就需要自定义控件/自定义View来满足我们的要求。为了掌握自定义View,我们需要了解View的底层工作原理,了解View的测量流程,布局流程以及绘制流程,还有View常见的回调方法,比如构造方法,onAttach,onVisibilityChanged,onDetach等,以及滑动效果处理。
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层。
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);
DecorView添加窗口到View的过程
View的绘制流程就是从ViewRoot的performTraversals方法开始的,它经过measuer,layout和draw三个过程才能最终将一个View绘制出来,针对performTraversals的大致流程。如下图所示:
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,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
// 获取测量到的子 view 高度
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
//第2步, 重新计算 LinearLayout 的 mTotalLength 总高度
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
..........................
//以下方法是对 LinearLayout 宽度相关的测量工作,不是我们关心的
if (widthMode != View.MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
.........................
//以上方法是对 LinearLayout 宽度相关的测量工作
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
//第3步,如果设置了 android:measureWithLargestChild="true"并且测量模式为 AT_MOST或者 UNSPECIFIED
// 重新计算 mTotalLength 总高度
if (useLargestChild &&
(heightMode == View.MeasureSpec.AT_MOST || heightMode == View.MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
//每个子视图的高度为:最大子视图高度 + 该子视图的上下外边距
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
//第4步,根据 heightMeasureSpec 测量模式 和已经测量得到的总高度 heightSize
//来确定得到最终 LinearLayout 高度和状态
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
//分割线=================以上代码就完成了对 LinearLayout 高度和状态 的测量
//第5步,下面代码是根据已经测量得到的 LinearLayout 高度来重新测量确定各个子 view 的大小
//获取 LinearLayout 高度值
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
//获取最终测量高度和经过测量各个子 view 得到的总高度差值
int delta = heightSize - mTotalLength;
//第5.1步(第5步中第1小步),如果在上面第一次测量子 view 的过程中有未进行测量的 view 那么执行下面代码
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
// 计算 weight 属性分配的大小,可能为负值
int share = (int) (childExtra * delta / weightSum);
weightSum -= childExtra;
delta -= share;
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
// TODO: Use a field like lp.isMeasured to figure out if this
// child has been previously measured
if ((lp.height != 0) || (heightMode != View.MeasureSpec.EXACTLY)) {
// 子视图在第一次测量时候已经测量过
// 基于上次测量值再次进行新的测量
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
// 调用子 view 的 measure 方法进行测量,后面逻辑就是 view 的测量逻辑
child.measure(childWidthMeasureSpec,
View.MeasureSpec.makeMeasureSpec(childHeight, View.MeasureSpec.EXACTLY));
} else {
// 子视图第一次测量,即第一步进行测量的时候未得到测量
//对 view 进行测量
child.measure(childWidthMeasureSpec,
View.MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
View.MeasureSpec.EXACTLY));
}
// Child may now not fit in vertical dimension.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
// 处理子视图宽度
final int margin = lp.leftMargin + lp.rightMargin;
...........................
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
//第5.2步(第5步中第2小步)执行到这里的代码,表明 view 是已经测量过的
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
// We have no limit, so make all weighted views as tall as the largest child.
// Children will have already been measured once.
if (useLargestChild && heightMode != View.MeasureSpec.EXACTLY) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
//如果 view 使用了权重即 childExtra > 0,使用最大子视图高度进行重新测量
//否则不进行测量,保持第一次测量值,那么由于 LinearLayout 的高度使用了子 view 最大高度 ,
// 但是子视图没有进行重新测量,没有进行拉伸,可能造成空间剩余。
if (childExtra > 0) {
//使用最大子视图高度进行重新测量子 view
child.measure(
View.MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(largestChildHeight,
View.MeasureSpec.EXACTLY));
}
}
}
}
if (!allFillParent && widthMode != View.MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
//第6步,最终设置 LinearLayout 的测量高宽
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
从上面的这段代码可以看出,系统会遍历子元素并对每个子元measureChildBeforeLayout 方法,这个方法内部会调用子元素的measure方法,这样各个子元素就可以一次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每次测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度和子元素在竖直方向上的margin等。
以上代码就是对 LinearLayout onMeasure 分析过程,整个过程原理已经在代码中加以注释说明,这里我们重点分析一下 resolveSizeAndState(heightSize, heightMeasureSpec, 0) 这个方法是如何实现最终确定 LinearLayout 高度值的,方法如下:
/**
* @param size view 想要的大小,也就是根据子 view 高度测量得到的高度值.
* @param measureSpec 父容器的约束条件
* @param childMeasuredState 子 view 的测量信息
* @return Size 返回得到的测量值和状态
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
//获取测量模式
final int specMode = MeasureSpec.getMode(measureSpec);
//获取尺寸值
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
//根据不同测量模式决定最终测量结果
switch (specMode) {
//如果是 AT_MOST 最大测量模式 ,那么总高度值为测量得到的 size 值,但是最大不能超过 specSize 规定值
case MeasureSpec.AT_MOST:
if (specSize < size) {
//如果测量得到的 size 值超过 specSize 值,LinearLayout 高度就为 specSize 值
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
//如果测量得到的 size 值未超过 specSize 值,LinearLayout 高度就为 size 值
result = size;
}
break;
case MeasureSpec.EXACTLY:
//如果是 EXACTLY 精准测量模式,即 LinearLayout 值为固定值,那么 最终 LinearLayout 高度值就为 specSize 值
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
// 如果是 UNSPECIFIED 测量模式,即对子 view 没有限制 , LinearLayout 高度值就为 size
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
以上代码总结起来就是 LinearLayout 会根据测量子 View 的情况和 MeasureSpec 约束条件来决定自己最终的大小,具体来说就是如果它的布局中高度才用 具体数值,那么它的测量过程和 View 一致,即高度为 specSize 值,如果它的布局中使用 wrap_content 那么它的高度是所有子 View 高度总和,但是不能超过父容器剩余空间。
最后对整个测量过程总结一下就是分为以下几步:
- 对 LinearLayout 中的子 View 进行第一次遍历测量,主要是通过 measureChildBeforeLayout 这个方法,这个方法内部会调用 measureChildWithMargins 方法,而在 measureChildWithMargins 方法内部会去调用 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 方法进行测量。在这次的测量过程中,如果满足了第1.1步测量条件的子 view 不需要进行测量,会在后面的第5.1步中进行测量。
- 根据测量各个子 View 的高度会得到一个初步的 LinearLayout 总高度 mTotalLength 值。
- 如果 LinearLayout 设置了 android:measureWithLargestChild=”true” 属性并且测量模式为 AT_MOST或者 UNSPECIFIED 重新计算 mTotalLength 总高度。
- 根据 LinearLayout 的 heightMeasureSpec 测量模式 和已经测量得到的总高度 mTotalLength ,来确定得到最终 LinearLayout 高度和状态 。
- 根据已经测量得到的 LinearLayout 高度来重新测量确定各个子 View 的大小。
- 最终执行 setMeasuredDimension 方法设置 LinearLayout 的测量高宽。
5. 在实际measure中View无法获取宽高信息的问题解决
View的measure过程和Activity的生命周期实际上是不同步的。所以将当View还没有测量完毕,那么获取的宽高就是0,这里有四种方法解决这个问题:
1. Activity/View#onWindowFocusChanged
onWindowFocusChanged方法表示View已经初始化了,宽高已经准备好了,这时候获取就没有问题。需要注意的是,onWindowFocusChanged会被调用多次,当Activity的窗口得到和失去焦点时均会被调用一次。具体说,当Activity继续执行和暂停执行的时候,onWindowFocusChanged均会被调用,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用。代码如下:
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if(hasWindowFocus){
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
}
2. view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也初始化好了,代码如下:
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
}
3. ViewTreeObsever
使用ViewTreeObsever的众多回调可以完成这个功能,比如使用onGlobalLayoutListener 这个接口,当View树状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout 方法将被回调。伴随着View树的变化,这个方法也会被多次调用。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
}
4. view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对View进行measure来得到View的宽高。这种方式比较复杂,需要分情况处理。根据View的LayoutParams来分:
(1) match_parent :无法measure出具体的宽高。因为在View的measure过程中,构造这种MeasureSpec需要知道parentSize,即知道父容器的剩余空间,match_parent是一个不确定值,所以理论上不能测量大小。
(2) wrap_content : 可以采用设置最大值方法进行 measure :
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
通过分析 MeasureSpec 的实现可以得知 View 的尺寸是使用 30 位的二进制表示,也就是说最大是 30 个 1 即(2^30-1),也就是 (1 << 30) - 1 ),在最大化模式下,使用 View 能支持的最大值去构造 MeasureSpec 是合理的。为什么这样就合理呢?我们前面分析在子 View 使用 wrap_content 模式的时候,其测量规则是根据自身的情况去测量尺寸,但是不能超过父容器的剩余空间的最大值,换句话说就是父容器给子 View 一个最大值,然后告诉子 View 你自己看着办,但是别超过这个尺寸就行,但是现在我们自己去测量的时候不知道父容器给定的 MeasureSpec 情况, 也就是不知道父容器给多大的限定值,需要自己去构造一个MeasureSpec ,那么这个最大值我们给定多少合适呢?所以这里干脆就给一个 View 所能支持的最大值,然子 View 根据自身情况去测量,怎么也不能超过这个值就行了。
关于View的measure,有两个常见的错误用法。理由是首先其违背了系统的内部实现规范(因为不能通过错误的MeasureSpec来得到合法的SpecMode,从而导致measure过程出错),其次是不能保证一定能measure出正确的结果。
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1,MeasureSpec.UNSPECIFIED);
view.measure (widthMeasureSpec,heightMeasureSpec);
view.measure(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
(3)具体的数值(dp/px): 例如100px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
6. View的Layout过程
Layout过程是ViewGroup的用来确定子元素的位置的,当ViewGroup的位置确定了之后,它在onLayout中会遍历所有的子元素,并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout过程比较简单,layout方法确定了View本身的位置,onlayout方法会确定所有子元素的位置,代码如下:
/*
*@param l view 左边缘相对于父布局左边缘距离
*@param t view 上边缘相对于父布局上边缘位置
*@param r view 右边缘相对于父布局左边缘距离
*@param b view 下边缘相对于父布局上边缘距离
*/
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;
}
//记录 view 原始位置
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//第1步,调用 setFrame 方法 设置新的 mLeft、mTop、mBottom、mRight 值,
//设置 View 本身四个顶点位置
//并返回 changed 用于判断 view 布局是否改变
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//第二步,如果 view 位置改变那么调用 onLayout 方法设置子 view 位置
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//调用 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);
}
}
}
layout 方法大致流程:先通过上面代码第一步调用 setFrame 设置 view 本身四个顶点位置,其中 setOpticalFrame 内部也是调用 setFrame 方法来完成设置的,即为 View 的4个成员变量(mLeft,mTop,mRight,mBottom)赋值,View 的四个顶点一旦确定,那么 View 在父容器中的位置就确定了,接着进行第二步,调用 onLayout 方法,这个方法用途是父容器确定子 View 位置,和 onMeasure 方法类似, onLayout 方法的具体实现同样和具体布局有关,所以 View 和 ViewGroup 中都没有真正实现 onLayout 方法,都是一个空方法。 View 的 onLayout 方法:
@Override
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);
}
}
和 onMeasure 类似,这里也是分为竖直方向和水平方向的布局安排,二者原理一样,我们选择竖直方向的 layoutVertical 来进行分析,这里给出主要代码如下:
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
//记录子 View 上边缘相对于父容器上边缘距离
int childTop;
//记录子 View 左边缘相对于父容器左边缘距离
int childLeft;
//第1步,主要是根据不同的 gravity 属性来确定子元素的 child 的位置
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
...............................
//第2步,循环遍历子 view
for (int i = 0; i < count; i++) {
//获取指定位置 view
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
//第2.1步,如果 view 可见,获取 view 的测量宽/高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//获取 view 的 LayoutParams 参数
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
.............
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
//第3步,设置子 view 位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
//第4步,重新计算子 view 的 顶部 top 位置,也就是每增加一个子 view
//下一个子 view 的 top 顶部位置就会相应的增加
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
简单梳理下整个流程,此方法会遍历所有子 view ,并调用 setChildFrame 方法来设定子元素位置,然后重新计算 childTop ,childTop 随着子元素的遍历而逐渐增大,这就意味着后面的子元素会被放置在当前子元素的下方,这正是我们平时使用竖直方向 LinearLayout 的特性。这里我们看一下第三步执行的 setChildFrame 方法类设置子元素位置方法代码:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
我们注意到,setChildFrame中的width和height 实际上就是子元素的测量宽高,从下面的代码可以看出:
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
setChildFrame(child,childLeft,childTop+getLocationOffset(child),childWidth,childHeight);
在layout方法中会设置setFrame去设置子元素的四个顶点的位置,
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
也就是说在 LinearLayout 中其子视图显示的宽和高由 measure 过程来决定的,因此 measure 过程的意义就是为 layout 过程提供视图显示范围的参考值。为什么说是提供参考值呢?因为 layout 过程中的4个参数 left, top, iwidth, height 完全可以由视图设计者任意指定,而最终视图的布局位置和大小完全由这4个参数决定,measure 过程得到的mMeasuredWidth 和 mMeasuredHeight 提供了视图大小的测量值,只是提供一个参考一般情况下我们使用这个参考值,但我们完全可以不使用这两个值,而自己在 layout 过程中去设定一个值,可见 measure 过程并不是必须的。
说到这里就不得说一下 getWidth() 、getHeight() 和 getMeasuredWidth()、getMeasuredHeight() 这两对函数之间的区别,即 View 的测量宽/高和最终显示宽/高之间的区别。首先我们看一下 getWith() 和 getHeight() 方法的具体实现:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
通过 getWith() 和 getHeight() 源码和上面 setChildFrame(View child, int left, int top, int width, int height) 方法设置子元素四个顶点位置的四个变量 mLeft、mTop、mRight、mBottom 的赋值过程来看,默认情况下 getWidth() 、getHeight() 方法返回的值正好就是 view 的测量宽/高,只不过 view 的测量宽/高形成于 view 的measure 过程,而最终宽/高形成于 view 的 layout 方法中,但是对于特殊情况,两者的值是不相等的,就是我们在 layout 过程中不按默认常规套路出牌,即不使用 measure 过程得到的 mMeasuredWidth 和 mMeasuredHeight ,而是人为的去自己根据需要设定的一个值的情况,例如以下代码,重写 view 的 layout 方法:
public void layout(int l, int t, int r, int b) {
//在得到的测量值基础上加100
super.layout(int l, int t, int r+100, int b+100);
}
上面代码会导致在任何情况下 view 的最终宽/高总会比测量宽高大100px。
7. View的draw过程
Draw过程比较简单,它的作用就是讲View绘制到屏幕上。View的绘制过程有以下几步:
- 绘制背景 background.draw(canvas)
- 绘制自己 (onDraw)
- 绘制children(dispatchDraw)
- 绘制装饰 (onDrawScrollBars)
源码如下:
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
//调用 onDraw 方法,绘制自己本身内容,这个方法是个空方法,没有具体实现,
//因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现,
//如果要自定义 view ,需要重载该方法完成绘制工作
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
//绘制子视图
//View 中的 dispatchDraw()方法也是一个空方法,因为 view 本身没有子视图,所以不需要,
//而 ViewGroup 的 dispatchDraw() 方法中就会有具体的绘制代码,来实现子视图的绘制工作
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)
//绘制装饰
//对视图的滚动条进行绘制,其实任何一个视图都是有滚动条的,只是一般情况下都没有让它显示出来,
//而例如像 ListView 等控件是进行了显示而已。
onDrawForeground(canvas);
// we're done...
return;
}
View的绘制过程的传递是dispatchDraw来实现的,dispatchDraw会遍历调用所有的子元素的draw方法,如此draw事件就一层层地传递下去。View有一个特殊的方法setWillNotDraw,先看看它的源码:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
看注释部分大概意思是,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true 后,系统会进行相应的优化,默认情况下 View 没有启动这个默认标记位,但 viewGroup 默认启用这个标记位,这个标记位对实际开发的意义是:当我们的自定义的控件继承自 viewGroup 并且本身不具备绘制功能的时候,就可以开启这个标记位,从而便于系统进行后续的优化工作,当我们明确知道 viewGrop 需要通过 onDraw 来绘制本身内容时,需要我们去关闭 WILL_NOT_DRAW 这个标记位。
参考链接:
http://yongyu.itscoder.com/2016/09/11/view_measure/
http://yongyu.itscoder.com/2016/10/05/view_layout_and_draw/
网友评论