首先研究ViewGroup,需要同时考虑measure自己部分和measure child, 虽然LinearLayout最常用,但是牵涉到weight,算法比较复杂,所以挑了一个简单的帧布局 FrameLayout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* {@inheritDoc}
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren .clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE ) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams ) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren .get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
getPaddingLeftWithForeground() - getPaddingRightWithForeground() -
lp.leftMargin - lp.rightMargin,
MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
if (lp.height == LayoutParams.MATCH_PARENT) {
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
getPaddingTopWithForeground() - getPaddingBottomWithForeground() -
lp.topMargin - lp.bottomMargin,
MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
首先回忆下FrameLayout的特性,帧布局 所有放在布局里的控件,都按照层次堆叠在屏幕的左上角,后加进来的控件覆盖前面的控件
第一次for循环 mMeasureAllChildren标记值无视~ 只计算非gone的child 首先先用measureChildWithMargins计算一下child,点开这个方法内部发现是调用的getChildMeasureSpec方法 即用上面表格的那个默认处理方法对所有的child都measure一遍~ 对child measure以后就可以获取到child的measuredWidth/Height/State了 然后根据获取到的宽高参数计算maxWidth/Height和childState 其中maxWidth/Height计算很简单,直接用Math.max所有的child,最终就获取到child里最大的宽高了 childState的计算是利用combineMeasureStates方法,将child的宽高的期望类型也用移位或的方法合并, 即childState是由width和heigh的期望类型共同组成的 每次循环最后,判断如果onMeasure的宽高模式其中有一个不是EXCTLY的,并且child的宽高参数类型都是MATCH_PARENT, 就将这个child保存至集合measureMatchParentChildren中,用作后面child的特殊measureSpec设置 第一轮循环结束 开始对自己进行measureSpec设置,直接用setMeasuredDimension方法, 而宽高的measureSpec计算需要利用resolveSizeAndState方法~ 在上一章中介绍过这个方法,作用就是将parent的期望类型和期望值width/heightMeasureSpec 和自己算出来的大小maxWidth/Height结合起来考虑,决定最终child的大小和类型
再下面是特殊child的measure处理了~ 这里可能会有疑惑,上面所有的child其实都默认measureChildWithMargins处理过一遍了, 这里为什么又要measure一次呢? 想象下特殊情况,如果FrameLayout宽或高是WRAP_CONTENT的,即这个宽或高将由child中最大的值决定, 而其中有一个child是MATCH_PARENT的,意思是大小和FrameLayout一样~ FrameLayout的大小只能在for循环所有child的以后才能确定maxWidth/Height, 即无法在第一轮for循环进行时就获取到parent的最终大小,自然没办法决定MATCH_PARENT的child大小了 所以在第一轮循环时先将特殊的MATCH_PARENT属性的child保存到一个集合里, 在第一轮循环结束后算出parent宽高以后,再开始对特殊的child进行二次循环处理, 对child的特殊处理为: 如果child是MATCH_PARENT, 则利用FrameLayout自己的宽高去除padding和margin后生成一个EXACTLY的measureSpec, 还要加个else判断不是MATCH_PARENT的情况, 因为上面是宽高其中有一个属性满足要求,就添加到集合里需要特殊处理,另一个参数可能是正常的 所以宽高都在else中添加上默认处理getChildMeasureSpec 最终用生成的measureSpec值再child.measure设置一次,搞定~
下面研究View的子类,举个最基本的例子TextView 由于是非ViewGroup,所以只有对自己身measure部分
代码为了理解方便,会减去child自己想要宽高的计算过程~ 即根据textSize,lines,drawableTop等综合算出大小,这个算法比较复杂,我们也暂时没有研究必要 宽高同理,所以只把宽度计算部分抽取出来,简化后代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec. getMode(widthMeasureSpec);
int widthSize = MeasureSpec. getSize(widthMeasureSpec);
int width;
if (widthMode == MeasureSpec. EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
} else {
width = .......... // 根据TextView一些特性如hint,ems,drawable等计算width值
// Check against our minimum width
width = Math. max(width, getSuggestedMinimumWidth());
if (widthMode == MeasureSpec. AT_MOST) {
width = Math. min(widthSize, width);
}
}
setMeasuredDimension(width, height);
}
同样是按照套路来的
- 如果是EXACTLY模式,那告诉我多大我就多大widthSize
- 如果是AT_MOST模式,那取计算出来的宽度(根据字体大小和字数多少)和parent告诉我的宽度中小的那个,即取一个不超过AT_MOST模式parent的期望数值
- 如果是UNSPECIFIED模式,则无视parent直接用child自己算出来的大小width
这一段if else的处理其实就等同于FrameLayout中的resolvSizeAndState方法处理, 但由于是View只能作为child,所以setMeasuredDimension就不需要考虑类型问题了
以上最基本的ViewGroup和View子类都介绍完了 作为android sdk中自带的类,肯定都是很靠谱按套路来的,即遵从规则的孩纸,相当于几个具有不同特性的标杆 如果再有特殊需要,可以考虑继承这几个实现子类,而不用直接继承View/ViewGroup, 这样就更加简单了,不用面面俱到,只用考虑自己所需的特殊实现,下面举几个间接继承的例子
ScrollView,可以滚动的,child只有一个且高度可以大于自己(此时可以上下滚动) 从标杆中找~ 果断用FrameLayout,这个类申明中有说明 一般来说只持有一个child(当然可以add多个view,但这个是推荐的最常见应用场景)
特性呢? child的height我们希望不做限定,即child想多高就多高, 如果上面已经理解了,这里肯定能直接想到用UNSPECIFIED模式
回忆下上面的FrameLayout,第一次for循环的时候用measureChildWithMargins计算了所有的child, 而measureChildWithMargins中用的是默认处理方式getChildMeasureSpec方法,即上一章中的那个表格 明显不是我们要的,那复写measureChildWithMargins之~ (这里如果直接复写onMeasure方法也是一样道理,最终目的都是为了让child的height类型设为UNSPECIFIED)
再查看ScrollView源码的measureChildWithMargins可以看到系统的具体处理
1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, intwidthUsed,
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 = MeasureSpec.makeMeasureSpec(
lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
宽度不变还是用默认方法getChildMeasureSpec 高度部分则用宽度则直接用MeasureSpec.makeMeasureSpec方法生成了一个UNSPECIFIED的宽度measure值
注: UNSPECIFIED的意思是child你想多大就多大,不用考虑我~ 所以如果用这个模式的话,数值部分就没用了, 一般直接makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);即可, 看默认处理方法getChildMeasureSpec也可以发现,child期望类型定为UNSPECIFIED时,size就直接设为0了
但是要再次强调,模式只是parent对child的一个期望,虽然UNSPECIFIED的意思是不限定按照child的自己来, 但万一child非要用到margin值呢,那就传吧~
总结 研究了很长时间,网上详细分析的资料也很少,文章来来回回修改了N次 每次觉得已经看懂了过两天又觉得又问题,最后终于勉强懂了大概 现在从头到尾梳理了一遍发出来,可能还是存在很多问题希望大家共同讨论,可能有不太准确的地方, 发表以后可能还会补充说明修改~
android的view显示是最核心的内容,主要分measure layout draw三个部分,而measure又是最复杂最难理解的
文章看懂以后不求能完全写出来一个LinearLayout这样sdk级别的控件, 能在写自定义控件知道onMeasure如何处理就可以了~不会疑惑”诶我操!我的控件为什么不显示”就行了 更主要的是能理解android中view的measure原理,知道大概流程是什么~