View 的视图系统组成
点击 App 后~
在开发的过程中,对于界面的绘制我们实际上接触到的是写 XML 文件。
绘制绘制,我们需要知道要对哪些 View 进行绘制,因为展示在我们面前的界面不可能绘制我们写的 XML 所对应的 View,View 的绘制其实包含了很多部分。
整个 View 的视图组成参考# 通俗易懂 Android视图系统的设计与实现的第二小节,写得非常清楚。
也参考自:Android从Activity启动到View显示中间发生了什么?

布局与测量
参考扔物线自定义 View
引言:一般来说,作为开发者我们不需要去关心页面内部是如何布局的。我们只管根据业务需求,编写好 xml 布局文件,系统就会帮我们处理好这些。但是,有些时候系统自带的控件没办法满足我们的要求。
比如,要写一个正方形的 ImageView,怎么办?可能会回答将其 layout_width与 layout_height设置成一样大就好了。但是如果不能确定这两个值,而是要根据父view 动态决定呢?
又比如标签布局(可以试着自己去实现一下)
这就得自己去定义他的尺寸算法了
测量阶段
Android 里的布局,其实包含两个过程:测量过程和布局过程。
首先,界面会从最顶级的根 View 向下递归地测量出每一级、每一个子 View 的尺寸和位置。 然后就是绘制过程,每一个 View 根据自己的尺寸和位置,进行自我绘制。
具体分析就是,当布局过程到来的时候。首先,一个 View 的 measure 方法会被他的父 View 调用。这个方法的作用是让这个 View 进行自我测量,不过真正执行测量的,不是 measure 方法,而是被调用的 onMeasure()方法。
measure 方法在被调用的时候,会做一些测量的预处理工作(计算 MeasureSpec 的值),然后去调用 onMeasure 方法去进行真正的自我测量。
这个自我测量的内容包含两种情况:(有没有子 View)
- View:没有子 View,做的事情只有一件,计算出自己的尺寸。
- ViewGroup:调用所有子 View的 measure 方法,让他们进行自我测量。根据子 View 自我测出来的尺寸,来计算出它们的位置,并且把它们的尺寸和位置保存下来。同时,还会根据这些子 View 的尺寸和位置,最终得出自己的尺寸。

布局阶段
在测量阶段,每一个 View 或者 ViewGroup 也会被父 View 调用它的一个方法:layout 。这个方法会对 View 进行内部布局。
与 measure 一样,layout 方法也是个调度方法,他在内部会做两件事。
- 保存 layout 方法传入的位置和尺寸
- 调用 onLayout 方法,也就是调用每一个子 view 的 layout 方法

这就是内部布局,也就是根据测量结果摆放所有子 View 的意思。

layout这一步 就相当于告述子 View:给你你的坐标。你已经有了自己的尺寸了,去吧,去绘制吧。
布局过程的自定义
分为三种类型,由简入难。
一:重写布局过程的相关方法
- 测量过程:onMeasure()
- 布局过程:onLayout()
- 具体
重写onMeasure()来修改已有的 View 的尺寸。 这一类是对于已有的 View,他已经有了自己的尺寸算法,他的 onMeasure()方法已经正确的计算出它的尺寸了。不需要重新计算一遍,根据自己的需求进行调整。

二. 重写 onMeasure()来全新计算自定义 View 的尺寸。
与第一类的区别: **需要完全计算自己的尺寸**
注意点:
- 重写 onMeasure,也不用调用 super.onMeasure();
- 计算出来的尺寸,要满足父 View 的限制;
widthMeasureSpec与heightMeasureSpec包含了父 View 对子 VIew 的尺寸限制。
问题:
- 这个限制怎么来的?
答:开发者对于子 View 的要求,也就是在 xml 布局文件中,里面的layout_打头的属性。它们是用来设置view 的位置和尺寸的,如很常用的layout_width layout_height layout_gravity layout_weight;

如图所示为很常规的xml 布局文件,比如android:text =""这些是给 view 自己处理的,规定显示什么内容。而android:layout_width这些是给父 View 处理的。
也就是说,在程序运行的时候,每一个ViewGroup会去读取它的子 View 的这些 layout_打头的属性,然后用它们去进行处理和计算。
- 子 View 的 onMeasure()应该怎么做来让自己符合这个限制? 答:

需要先知道的是,父 View 传进来的参数,也就是宽度高度的限制,measureSpec是一个压缩数据,包含了类型与尺寸值( int 应该是前几位代表类型 mode,后几位代表尺寸值)。
限制一共有三种:
- UNSPECIFIED:无限制,会直接把子 view 的计算结果返回。
- AT_MOST:限制上限。父 View 限制子 View 的上限是 500.
- EXACTLY:限制固定尺寸。

总结:
-
重写 onMeasure()方法把尺寸计算出来;
-
把计算的结果用 resolveSize()过滤一遍然后再保存;
三. 重写 onMeasure()和 onLayout() 来全新计算 ViewGroup 的内部布局。
第一步 重写 onMeasure()来计算内部布局。
1.调用每个子 View 的 measure(),让子 View 自我测量
这一步看起来很简单,就是遍历子 View,调用其 onMeasure 方法。确实是这个思路,但是别忘了,在调用子 View 的 mearsure 方法时,得传进去对子 View 的宽高限制。而 ViewGroup 如何确定自己对子 View 的宽高限制,是个麻烦事。
ViewGroup 对子 View 的宽高限制,来源于两个地方:开发者在 xml中的要求、自己的可用空间。
首先是开发者的要求,例如在 xml 中对子 View 写上 layout_width\layoutHeight.
子 View 中调用getLayoutParams获得的 LayoutParams 对象,存储了 xml 文件里layout_打头的参数的对应值。


解释上图:
首先,最简单的就是在布局里写定了值,这种直接使用该值和设置模式为 EXACTLY;
MATCH_PARENT:如果自己的模式是无限制,则子 View 也别限制了。如果不是无限制,则用自己的可用空间减去已经使用的空间。
WRAP_CONTENT:如果自己的模式是无限制,则子 View 也别限制了。如果不是无限制,则用自己的可用空间减去已经使用的空间。
2.根据子 View 给出的尺寸,得出子 View 的位置,并保存它们的位置和尺寸。
测量子 View 的过程,其实就是一个排班过程,那么在测量过程中就能够拿到每一个子 View 的位置,然后再保存下来。(这句听的云里雾里,还是不了解怎么样能拿到每一个子 VIew 的位置,只是知道调用完子 View 去测量之后,会同时存下来子 View 的位置)
尺寸简单,99%的情况每一个 View 所测得的尺寸就是他的最终尺寸,调用 getMeasureWidth就行。
关于保存子 View 位置的两点说明
- 不是所有的 Layout 都需要保存子 View 的位置,因为有的例如 LinearLayout,他的内容都是横向或者竖向一字排开的,可以在布局阶段实时推导出子 View 的位置。
- 特殊情况下对某些子 View 需要重复测量两次或多次才能得到正确的尺寸。
3. 根据子 View 的位置和尺寸来计算出自己的尺寸,并用 setMeasuredDimension()保存。
根据子 View 的排布计算出边界,这个边界就是你的尺寸了。
第二步 重写onLayout()来摆放子 View。

只需要注意,子 View 的 layout 的四个参数,是在它父 View 中的左上右下四个相对坐标。
总结:

面试回答
1.来讲讲 View 的绘制流程。
补充从 setContentView 到自己写的 xml 加入到 DecorView 的过程。
private void performTraversals() {
//desiredWindowWidth和desiredWindowHeight是屏幕的尺寸
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
//执行测量流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
//执行布局流程
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
//执行绘制流程
performDraw();
}
private static int getRootMeaureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATRCH_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;
}
我们在写 xml 的时候,外层都有个 layout 布局,layout 也是个 ViewGroup 。DecorView 在调用performMeasure时,会将具体的测量操作让内部的 ViewGroup 执行。
private void perormMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec)
{ ...
// 具体的测量操作分发给ViewGroup
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
// 在ViewGroup中的measureChildren()方法中遍历测量ViewGroup中所有的View
protected 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];
// 当View的可见性处于GONE状态时,不对其进行测量
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
// 测量某个指定的View
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
// 根据父容器的MeasureSpec和子View的LayoutParams等信息计算
// 子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// View的measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
// ViewGroup没有定义测量的具体过程,因为ViewGroup是一个
// 抽象类,其测量过程的onMeasure方法需要各个子类去实现
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
// 不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,如果需要自定义测量过程,则子类可以重写这个方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// setMeasureDimension方法用于设置View的测量宽高
setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
// 如果View没有重写onMeasure方法,则会默认调用getDefaultSize来获得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:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = sepcSize;
break;
}
return result;
}
//如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinmumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
mView为一个 ViewGroup,调用其 measure 方法会调用其 onMeasure 方法,因为其为一个 ViewGroup,所以会调用所有子 View的 measure 方法,让他们进行自我测量。
而在子 View 的自我测量中,是调用子 view 的 measure 方法,同时要传入MeasureSpec。这个MeasureSpec是子 View 在测量自己尺寸时宽度高度的限制。
这里可以引出问题2:当调用下一级 View的 measure 方法时,这个MeasureSpec的值应该怎么处理?
当子 View 调用自己的 onMeasure 方法,就开始了自我测量的过程,最后使用setMeasureDimension方法保存自己的宽度和高度。
ViewGroup 在遍历完所有子 View,让子 View 都测量出自己的尺寸时。会保存子 View 的尺寸并算出这些子 View 的位置。
参考: