接着上一章, view绘制过程-measure
layout概述
-
作用 本章介绍的是layout部分,即measure在计算完所绘制的东西多大后,由layout再去决定绘制在什么位置, 比如五大布局,最大的区别就在于child的排列布局规则,这个不同child的位置控制,就需要靠layout部分了
-
什么时候要处理 layout部分是决定child位置的,所以只有直接/间接继承ViewGroup时才要处理,继承View的不用处理
-
如何处理 同measure一样,都是要复写处理onLayout方法,layout方法无法复写 一般要计算出所需数据,然后child.layout(l, t, r, b)让child布局在我们想要的位置 计算时,主要是根据child的大小和布局参数等信息, 去决定不同类型的ViewGroup中child放在什么位置
与measure的关系
首先先介绍下两者实现上的区别 measure就是根据parent,自己,child的measureSpec和布局参数等等共同决定自己的最终大小, 以及决定对child的期望大小,听着就很绕,是绘制三部曲中最难的一块 而layout只要根据child的数据去决定child的位置即可,比measure要简单很多
然后是联系 onLayout复写时,l t r b四个参数是由当前ViewGroup所在位置和measure计算结果共同决定的 其中ViewGroup在布局文件中的位置决定起始位置即l t ,再根据measure算出的宽高大小决定r b
画个图帮助理解下,外面的大矩形是屏幕,里面的小矩形是我们的ViewGroup 如果ViewGroup在布局文件中距离左边和上面都有20的距离,即l=20, t=20 然后measure计算的ViewGroup宽高是200和150 那么在onLayout时四个参数就分别会是 l=20, t=20, r=200+20=220, b=150+20=170
同样,如果measure计算结果不变,这个ViewGroup移动到了屏幕的最左上角,那就是 l=0, t=0, r=200, b=150
所以反过来,在onLayout中,r-l就是ViewGroup的宽度,b-t就是高度了
layout流程实例分析 找系统控件源码来分析下,先来简单的Framelayout onLayout中调用了一个layoutChildren方法如下
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
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top
- getPaddingBottomWithForeground();
mForegroundBoundsChanged = true;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp. gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity,
layoutDirection);
final int verticalGravity = gravity
& Gravity. VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK ) {
case Gravity. CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width)
/ 2 + lp. leftMargin - lp.rightMargin;
break;
case Gravity. RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity. LEFT:
default:
childLeft = parentLeft + lp. leftMargin;
}
switch (verticalGravity) {
case Gravity. TOP:
childTop = parentTop + lp. topMargin;
break;
case Gravity. CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height)
/ 2 + lp. topMargin - lp.bottomMargin;
break;
case Gravity. BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp. topMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop
+ height);
}
}
}
直接for循环child,然后将所有非gone的child控件都进行布局设置 其中,最大的影响因素为child的大小和gravity属性
注意,上面代码的lp.gravity是布局文件中的android:layout_gravity属性而不是android:gravity 两者是有区别的 layout_gravity是控制当前view在parent中的位置 gravity是控制当前view里面child/内容的位置
继续分析for循环内的child处理代码 俩个switch代码,根据gravity分别处理横向和纵向的位置,横向的布局默认是left,纵向默认是top, 所以如果不对gravity设置,那么child会全部放在Framelayout内的left+top即左上角, 比如gravity是center_vertical,第一段switch代码horizontal的gravity判断时就是默认的left, 而下面第二段的verticalGravity就是对应的Gravity.CENTER_VERTICAL,则childTop的计算参考下图 其中top bottom都是坐标,而height为长度
数学几何题,如果想center_vertical效果,即垂直方向居中的话,就需要child的top位置为: childTop = parentTop + ( parentHeight - height ) / 2 其中parentHeight直接用parentBottom-parentTop即可得出,最后加上margin处理,就是系统的代码 而child的bottom位置就是top+height,最后算好top bottom left right后直接child.layout…
水平居中的话同理,如果layout_gravity是center的话,等价于center_vertical | center_horizontal, 即水平和垂直都是居中,会分别计算两个方向的center位置,最后把child放置在最中间
再举另一个具有代表性的分析LinearLayout,按照方向分两种,这里以vertical垂直线性排列为例分析 onLayout中垂直排列时会调用方法layoutVertical如下
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
92
93
/**
* Position the children during a layout pass if the orientation of this
* LinearLayout is set to {@link #VERTICAL}.
*
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onLayout(boolean, int, int, int, int)
* @param left
* @param top
* @param right
* @param bottom
*/
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK ;
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;
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp. gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity. HORIZONTAL_GRAVITY_MASK) {
case Gravity. CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp. leftMargin - lp. rightMargin;
break;
case Gravity. RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity. LEFT:
default:
childLeft = paddingLeft + lp. leftMargin;
break;
}
if ( hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp. topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp. bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
如果是垂直线性排列的话,那垂直方向就会一个挨一个的排列,而水平方向要考虑不同child决定水平位置
首先是根据垂直向gravity计算一下top和bottom位置,决定所有的child排列在哪个范围内,确定总范围 这里代码中的mTotalLength是所有child在垂直方向的总长度,是在measure中计算得出的
然后下面的for循环遍历所有child决定具体每一个子控件排列,这里分两部分, 一是在横向上决定child的位置,根据gravity计算left和right位置,与Framelayout介绍过的计算方法同理 另一部分是处理纵向上的, 把每个child纵向上的top位置按顺序挨个算出来
纵向上计算也很好理解,每次child的top都要再加上之前child累计的height还有divider和margin等 比如第一个child的top是100,height是10,那首先让第一个child.layout(left, 100, left+width, 100+10) 然后第二个child的top就应该是100+10(暂不考虑margin divider),所以第二个child就应该是 child.layout(left, 110, left+width, 110+10) … layout部分结束
实例练手 布局是决定child位置的,所以找一个具有代表性的布局特别的自定义控件,仿path的菜单 这里我们简单做一个path菜单的实现,只用作研究onLayout, 所以什么旋转动画位移动画啊就都不做了,点击展开显示啥的也略去,很多数据也写死了,方便理解
设计思路,自定义viewgroup,在里面add一个menu菜单按钮,还有5个二级菜单小按钮 小按钮围绕着大按钮成四分之一圆型排列,效果图如下
draw部分赞略,绘制系列技术