View的工作原理(一)初认识ViewRoot、DecorView,理解MeasureSpec
要点
一、初识RootView、DecorView
为了更好的熟悉view的三大流程(测量、摆放、绘制)我们就先普及下RootView、DecorView基本概念。
1、ViewRoot
1、ViewRoot 对应ViewRootImlp 类,它其实是连接 WindowManger 和 DecorView 的桥梁。View 的三大流程都是通过 ViewRoot 来完成的。
2、在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象与 DecorView 进行关联。
3、整个 View 的绘制流程,是从 ViewRoot 的 performTraversals 方法开始的。 它经过measure、layout、draw 这三个过程才最终将一个 View 绘制出来,其中 measure 是用于测量 View 的宽高的,layout 是用于当继承 ViewGroup 时确定子 View 位置的, draw 则是负责将 View 绘制在屏幕上。如下图(performTraversals 工作流程图)
4、performTraversals 会依次调用 performMeasure、performLayout、performDraw 这三个方法,这三个方法会依次完成* View 的 measure、layout、draw 三大流程。
5、其中 performMeasure 方法会接着调用 measure 方法,在 measure 方法中又会去调用 onMeasure 方法。在 onMeasure 方法中对所有的子元素进行了 measure 过程 ,这个时候 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着,子元素又重复了一遍 measure 过程,如此反复,直到没有了子元素。这样就完成了整个 View 树的遍历。
6、其中 layout 和 draw 的流程同上,两者同样都有 performLayout 、performDraw 和 layout、draw 方法,唯一不同的是,performDraw 的传递是在 draw 方法中通过 dispatchDraw 来实现的,不过这并没有什么本质上的区别。
2、三大流程简介
-
measure
这个过程决定了 View 的宽/高。measure 完成以后,可以通过 getMeasureWidth 和 getMeasureHeight 来获取View 测量后的宽/高。 -
layout
这个过程决定了 View 四个定点的坐标和实际 View 的宽/高。完成以后,可以通过 getTop、getLeft、getBottom、getRight 来获取 View 的四个顶点的位置,并可以通过 getWidth 和 getHeight 方法获取 View 的最终宽/高。
ps:其实getTop、getBottom、getRight、getLeft这四个方法是view的方法在view基础知识中有讲解参考:View的事件体系(一)view基础和view的几种滑动方式中的图解 -
draw
这个过程决定了 View 的显示,只有当 draw 方法完成以后,View 才能够显示到屏幕上。
3、DecorView
如上图(图片来源网络):
1、DecorView 在一般情况下,内部会包含一个竖直的 LInearLayout,里面有上下两部分(具体情况和安卓版本和主题有关),上面是标题栏,下面是内容,内容布局有一个默认的 id: content。
2、DecorView 继承自 FrameLayout,是一个 ViewGroup。在整个 ViewTree 中, DecorView 是整个 ViewTree 的顶层 View。View 的所有事件,都先经过 DecorView,然后再传递给 View
我们在 Activity 中 通过 setContentView() 方法设置的布局,其实就是添加到了内容部分里。如果我们想获取到内容布局的话,可以通过如下方法获取:
ViewGroup content = (ViewGroup) findViewById(android.R.id.content);
View childAt = content.getChildAt(0);
二、理解MeasureSpec
MeasureSpec 是 View 的一个内部类,代表了一个32位的 int 值。前 2 位代表 SpecMode,后 30 位代表 SpecSize。SpecMode 是指测量模式,SpecSize 是指在某种测量模式下的规格大小。
测量模式(SpecMode )有3种:UNSPECIFIED,EXACTLY,AT_MOST。我们只需要使用2个bit就可以做到,因为2个bit取值范围是[0,3]里面可以存放4个数足够我们用了。那么Google是怎么把一个int同时放测量模式和尺寸信息呢?我们知道int型数据占用32个bit,而google实现的是,将int数据的前面2个bit用于区分不同的布局模式,后面30个bit存放的是尺寸的数据。
该类在很大程度上决定了 View 的尺寸规格,除此之外父容器也影响 View 的 MeasureSpec 的创建过程。系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后再根据这个 MeasureSpec 来测量出 View 的宽高。需要注意的是,这个的宽高是测量的宽高,并不一定是 View 最终的宽高。后面会详细了解 MeasureSpec 的创建过程,在了解之前,首先来了解下 MeasureSpec 的三种测量模式。
参考下图理解:
布局参数参考:
从LayoutParams说起到代码动态布局
关于LayoutParams的使用
1、三种测量模式
SpecMode 和 SpecSize 组成了 MeasureSpec,MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象创建,并提供了对应的打包、解包方法
首先我们看看MeasureSpec源码。
public static class 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;
// 打包操作
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
}
// 解包 获得 mode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
//解包 获得size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
2、SpecMode 三类
-
UNSPECIFIED:
父容器不对子元素作任何约束,子 View 想要多大就给多大。这种情况一般用在系统内部,表示一种测量的状态。一般情况下,我们不用关注该测量模式。 -
EXACTLY:
精准模式。父容器已经解决了子元素所需要的精准大小,这时候子 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值。 -
AT_MOST:
最大模式。父最大模式容器指定了一个可用大小即 SpecSize,子元素最大不可以超过指定的这个值。它对应于LayoutParams 中的 wrap_content。
3、MeasureSpec和LayoutParams 的关系
我们知道:对于普通 View,MeasureSpec 是由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定的。
看看源码,MeasureSpec 是怎么被创建出来的,由于普通 View 的 measure 过程由 ViewGroup 传递而来,所以首先来看一下 ViewGroup 的:measureChildWithMargins()方法
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子元素的布局参数
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 根据父容器WidthMeasureSpec,子view自身布局参数 ,子view的margain、padding来获得 子view的WidthMeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
// 根据父容器WidthMeasureSpec,子view自身布局参数 ,子view的margain、padding来获得 子view的WidthMeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//开始测量子view
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
布局参数参考:https://blog.csdn.net/u012810020/article/details/51970771
首先获取到了子元素的 LayoutParams 然后根据当前子元素的 LayoutParams 和父类的MeasureSpec,获取了子元素的 MeasureSpec,最后调用了子元素的 measure。
继续深入getChildMeasureSpec源码如下:
//参数 父容器MeasureSpec,父容器padding,子view大小
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);//父容器mode
int specSize = MeasureSpec.getSize(spec);//父容器size
int size = Math.max(0, specSize - padding); //父容器可用size空间==父容器总空间-padding空间
int resultSize = 0;//测量出的子元素size 先默认为0;
int resultMode = 0;//测量出的子元素mode先默认为0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:// 父控件EXACTLY模式时
if (childDimension >= 0) {//子元素采用固定数值时(比如xml中宽高设置了具体dp)
resultSize = childDimension; // 测量出 子元素size 为childDimension
resultMode = MeasureSpec.EXACTLY;// 测量出 子元素模式为EXACTLY
} else if (childDimension == LayoutParams.MATCH_PARENT) {//子元素设置了MATCH_PARENT时
// Child wants to be our size. So be it.
resultSize = size;// 子元素大小为 size,这个size是父容器大小减去padding的空间
resultMode = MeasureSpec.EXACTLY;//子元素模式为EXACTLY
} else if (childDimension == LayoutParams.WRAP_CONTENT) {//子元素设置为WRAP_CONTENT时
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; // 子元素大小为 size,这个size是父容器大小减去padding的空间
resultMode = MeasureSpec.AT_MOST;//子元素模式为AT_MOST
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST://当父容器为AT_MOST模式时
if (childDimension >= 0) {//子元素设置了固定值时(比如宽高设置:50dp)
// Child wants a specific size... so be it
resultSize = childDimension; //大小为子元素大小
resultMode = MeasureSpec.EXACTLY;//模式为EXACTLY
} else if (childDimension == LayoutParams.MATCH_PARENT) {//子元素设置MATCH_PARENT时
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;//大小为父容器剩余空间(参考上文size解释)
resultMode = MeasureSpec.AT_MOST;//模式AT_MOST
} else if (childDimension == LayoutParams.WRAP_CONTENT) {//子元素设置WRAP_CONTENT时
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;//大小为父容器剩余空间(参考上文size解释)
resultMode = MeasureSpec.AT_MOST;//模式AT_MOST
}
//(可以看出自定义view时父容器的AT_MOST我们不处理时view设置match_parent、wrap_content都是match_parent效果)
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);
}
getChildMeasureSpec展示了普通view的MeasureSpec创建规则,根据源码总结出下表:
总结:(详细的分析请看注释,我已经几乎都注释了)
1、当 View 采用固定宽高(确切值)的时候,不管父容器的 MeasureSpec 是什么,View 的测量模式都是 EXACTLY 也就是精准模式。并且其大小遵循 LayoutParams 中的大小。
2、当 View 的宽高是 match_parent 的时候,如果父控件是精准模式,那么 View 也是精准模式,并且大小是父容器的剩余空间。如果父控件是最大模式,那么 View 也是最大模式,并且大小不会超过父容器的剩余空间。
3、当 View 的宽高是 wrap_content 的时候,不管父控件是精准模式还是最大模式,View 的模式最是最大模式,并且大小不能够超过父容器的剩余空间。
ps:UNSPECIFIED 系统使用,我们不考虑。
实际开发中:
实际开发中:
1、UNSPECIFIED我们不常见。几乎不用处理。
2、EXACTLY我们也几乎不用处理,
3、AT_MOST 这种我们要处理。(上面源码中AT_MOST状况下的注释如下)
(可以看出自定义view时父容器的AT_MOST我们不处理时view设置match_parent、wrap_content都是match_parent效果)
栗子如下:
首先自定义view继承view啥也不处理:
/**
* Create by SunnyDay on 2019/04/12
*/
public class MyLayout extends View {
public MyLayout(Context context) {
super(context);
}
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
直接使用:
结果:
如上:
自定义view时,除了第一个view的高度我们设置精确值时显示无误,我们设置为wrap_content时的效果就是match_parent的效果。
ps:中间写了个TextView以做隔离区分
如何处理参考view的三大流程
三、小结
总结了一篇,发现MeasureSpec还真不好理解。。。。。
参考文章:https://blog.csdn.net/Airsaid/article/details/53576087
The end
本文来自<安卓开发艺术探索>笔记总结