0%

View Layout

前言

Android View 体系 Layout 流程分析.

performTraversals

结合前文 View Measure 中 performTraversals 的代码继续往下看。

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
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;


WindowManager.LayoutParams lp = mWindowAttributes;



Rect frame = mWinFrame;


boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);

if (mWidth != frame.width() || mHeight != frame.height()) {
mWidth = frame.width();
mHeight = frame.height();
}

final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
}

这里的 lp 前面已经说过了,就是我们设置的 ContentView 的LayoutParams。mWidth, mHeight 又是什么呢?其实就是屏幕的宽高,这个我们可以反射调用验证一下。

反射获取 ViewRootImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private fun hackInfo(): Pair<Int, Int> {
var w = 0
var h = 0
try {
val clazz = this.javaClass
val getViewRootImpl = clazz.getMethod("getViewRootImpl")
val result = getViewRootImpl.invoke(parent.parent)
val mWidth = result.javaClass.getDeclaredField("mWidth")
val mHeight = result.javaClass.getDeclaredField("mHeight")
mWidth.isAccessible = true
mHeight.isAccessible = true
w = mWidth.get(result) as Int
h = mHeight.get(result) as Int

Log.e("hackInfo", "w=$w,h=$h")
} catch (e: Exception) {
}
return Pair(w, h)
}
1
E/hackInfo: w=1080,h=1920

performLayout

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
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mScrollMayChange = true;
mInLayout = true;

final View host = mView;
if (host == null) {
return;
}

Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

mInLayout = false;
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
// requestLayout() was called during layout.
// If no layout-request flags are set on the requesting views, there is no problem.
// If some requests are still pending, then we need to clear those flags and do
// a full request/measure/layout pass to handle this situation.
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
false);
if (validLayoutRequesters != null) {
// Set this flag to indicate that any further requests are happening during
// the second pass, which may result in posting those requests to the next
// frame instead
mHandlingLayoutInLayoutRequest = true;

// Process fresh layout requests, then measure and layout
int numValidRequests = validLayoutRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
Log.w("View", "requestLayout() improperly called by " + view +
" during layout: running second layout pass");
view.requestLayout();
}
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
mInLayout = true;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

mHandlingLayoutInLayoutRequest = false;

// Check the valid requests again, this time without checking/clearing the
// layout flags, since requests happening during the second pass get noop'd
validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
if (validLayoutRequesters != null) {
final ArrayList<View> finalRequesters = validLayoutRequesters;
// Post second-pass requests to the next frame
getRunQueue().post(new Runnable() {
@Override
public void run() {
int numValidRequests = finalRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = finalRequesters.get(i);
Log.w("View", "requestLayout() improperly called by " + view +
" during second layout pass: posting in next frame");
view.requestLayout();
}
}
});
}
}

}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}

可以看到 performLayout 从调用到实现都是比较容易看得懂的。首先是只要执行了

1
2
3
4
5
6
7
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

那么 mLayoutRequested = true 。因此可以认为只要调用了 requestLayout 就会执行 performLayout。在 performLayout 内部会调用 View.layout 方法。当然这里还需要处理发生在 layout 这个阶段执行 requestLayout 操作的 view。会对这些位进行一次完整的 measure/layout ,如果在这一次完整 measure/layout 的过程中有发生了requestLayout 的调用,那么就会把这一次的调用放到下一帧执行。

layout

layout 执行

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
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;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); // ⑥
// ②
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
// ③
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);
}
}
}

final boolean wasLayoutValid = isLayoutValid();
// ⑤
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

我们一次分析代码中比较关键的几个点

① 什么时候这个条件会成立呢?

在 view measure 中有这样一段代码

1
2
3
4
5
6
7
8
9
10
11
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

也就是说如果 measure 阶段是从 mMeasureCache 中直接获取的,跳过了 onMeasure 直接 setMeasuredDimensionRaw 。那么就会添加 PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT 这个标志位。这样,到了 layout 阶段就会用旧的 MeasureSpec 参数进行一次 onMeasure 调用。

② isLayoutModeOptical 一般为 false,因此如果要确保 onLayout 的执行,就得看 PFLAG_LAYOUT_REQUIRED 这个标志位了。这个还是得看 view 的 measure 方法。
1
2
3
4
5
if (forceLayout || needsLayout) {
// else logic

mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}

可以看到,不管通过任何方式只要是进行了真实的 measure 操作,不管是从缓存取值还是执行 OnMeasure 进行测量,都会设置这个标志位。

③和⑤ 可看到,无论如何最终都会清除 PFLAG_LAYOUT_REQUIRED 标志位。
④ 当有 onLayout 操作时,会有相应的回调方法被调用。
⑥ setFrame
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
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;

if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;

// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

// Invalidate our old position
invalidate(sizeChanged);

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;


if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}

if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
// If we are visible, force the DRAWN bit to on so that
// this invalidate will go through (at least to our parent).
// This is because someone may have invalidated this view
// before this call to setFrame came in, thereby clearing
// the DRAWN bit.
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
// parent display list may need to be recreated based on a change in the bounds
// of any child
invalidateParentCaches();
}

// Reset drawn bit to original value (invalidate turns it off)
mPrivateFlags |= drawn;

mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}

notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}

可以看到 setFrame 方法还是很关键的,坐标位置有任意一项发生变化,就认为 changed = true .这也是合理的。但是这里要注意的是,坐标变化不代表视图大小变化,比如从 (0,0,1,1) 变成 (1,1,2,2) 虽然 changed = true. 但是 sizeChanged 并不等于 true ,是不会触发 sizeChange 的。这里最重要的是完成了 mLeft,mTop,mRight,mBottom 几个成员变量的赋值,也就是说到这一步就已经确定了整个 View 在坐标系的位置,即其宽高值。

onLayout
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
94
95
96
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
```

可以看到 onLayout 对于 view 来说就是空实现,因为没有子 view 嘛。因此,这就需要 LinearLayout/FrameLayout/RelativeLayout 等这些各有特点的 ViewGroup 去实现。


## 整体流程

<img src="View-Layout/layout.svg">

## FrameLayout && LinearLayout layout 实现简析

下面就从 FrameLayout 这两个比较典型的 ViewGroup 的 onLayout 是实现细节出发,看看 onLayout 的实现有哪些需要考虑的细节。

### FrameLayout

如果让我们自己去实现一个 FrameLayout 的话?在 onLayout 中我们会怎么实现呢?
FrameLayout 相对来说比较简单,子 View 默认会以左上角为锚点按自身的宽高进行布局。

```java
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();

final int parentLeft = getPaddingLeftWithForeground();
// 这里 parentRight 的含义,可以理解为 FrameLayout 剩余的宽度
final int parentRight = right - left - getPaddingRightWithForeground();

final int parentTop = getPaddingTopWithForeground();
// 这里 parentBottom 的含义,可以理解为 FrameLayout 剩余的高度
final int parentBottom = bottom - top - getPaddingBottomWithForeground();

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;


// 首先确定在水平方向,子 view 的坐标
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);
}
}
}

可以看到 FrameLayout 对于子 view 的 layout 即简单又复杂,对于普通场景,就是按照左上角的锚点加上子view 自身的宽高即可,但是考虑到 FrameLayout 自身的 padding 以及 子view Gravity 属性的变化,就需要一些几何计算的技巧了。具体实现大家按照坐标系画图就可以理解了,可以说是非常巧妙。

LinearLayout

LinearLayout 作为常用的一种布局结构,可以实现垂直布局或水平不同,同时也可以设置 gravity 。因此,他的 layout 和 measure 是紧密相关的。因此,和
measure 相似,对于 layout 也是区分水平和垂直布局分别展开。这里看一下 layoutVertical.

我们可以想一下,如果自己去实现一个垂直布局的功能要考虑什么?其实如果先不管 gravity 等因素,简单一点的话只需考虑

  • LinearLayout 自身的 paddingLeft
  • 每一个子 view 的 left 和 top 值,然后加上子 view 的宽高位置不就确定了吗?
  • 不断累加左边的开始位置,放置一个子 view,假设宽度没有上限,那么就这么一直往左边摆不就可以了吗?

但是实际中屏幕宽度是非常有限的,因此看看 LinearLayout 是如何解决这些问题的吧。

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
94
95
96
@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);
}
}

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();

// 垂直布局,因此垂直方法的 Gravity 是 main,水平方向时 minor。有点 flexbox 那味了
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

// 可以先看默认值 Gravity.TOP ,再看其他的就比较好理解了
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:
// 对于每一个 子 view 在水平方向没有特殊要求的 paddingLeft 加上自身的 marginLeft 就可以了
childLeft = paddingLeft + lp.leftMargin;
break;
}

if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
// 首先加上 当前子 view 的 marginTop
childTop += lp.topMargin;
// 摆放这个 子view
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
// 累加 childTop 的之。这里可以看到 topMargin 和 botttomMargin 累加的位置,可以说是非常巧妙了。
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

i += getChildrenSkipCount(child, i);
}
}
}

自定义实现 MyFlowLayout

纸上来得终觉浅,绝知此事要躬行。

下面我们来实现一个非常实用的 MyFlowLayout 布局。当然了, Google 官方专门 提供了 com.google.android:flexbox:1.0.0 用于实现标签布局。这里实现是非常简单的,只是为了巩固 onMeasure 和 onLayout 的知识。

可以思考一下,面对上面这样一个布局标签实现,在用 LinearLayout、RelativeLayout 等传统布局方式无法实现的情况下,我们改去怎么实现呢?

如果还需要支持 margin,paddign,wrap_content 等场景,那么我们的 MyFlowLayout 自身宽高又该如何计算。宽高确定之后,每一个子 view 又该如何布局,每一个方几个,如何判断放不下了? 放不下了如何换行? 最后一行又该如何处理?

MyFlowLayout 之 measure

我们先来进行 measure 。 measure 总体的思路很简单,就是遍历每一个 子view,每一行的宽度做累加,如果超过了父布局剩余的宽度,那么就换行,同时在这个过程中记录每一行的最大高度,对高度做累加,这样就可以计算出整个 view 的高度了。

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
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

//获得宽高的测量模式和测量值
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
var widthSize = MeasureSpec.getSize(widthMeasureSpec)
var heightSize = MeasureSpec.getSize(heightMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)

var perLineWidth = 0 // 每一行总宽度
var perLineMaxH = 0 // 每一行最多高度
var maxH = 0 // 当前 ViewGroup 总高度

for (i in 0 until childCount) {
val childView: View = getChildAt(i)

measureChild(childView, widthMeasureSpec, heightMeasureSpec)
val marginLp = childView.layoutParams as MarginLayoutParams
val childWidth = childView.measuredWidth + marginLp.leftMargin + marginLp.rightMargin;
val childHeight = childView.measuredHeight + marginLp.topMargin + marginLp.bottomMargin

// 需要换行
if (perLineWidth + childWidth > widthSize) {
// 高度累加
maxH += perLineMaxH
// 开始新的一行
perLineWidth = childWidth
perLineMaxH = childHeight
} else { // 不需要换行
// 行宽度累加
perLineWidth += childWidth
// 当前行最大高度
perLineMaxH = Math.max(perLineMaxH, childHeight)
}
//当该View已是最后一个View时,将该行最大高度添加到totalHeight中
if (i == childCount - 1) {
maxH += perLineMaxH
}
}
if (heightMode != MeasureSpec.EXACTLY) {
heightSize = maxH
}
setMeasuredDimension(widthSize, heightSize)
}

这里我们对宽度没有特殊处理,如果没有写具体值的话,那么 wrap_content 和 match_parent 会是相同的效果。有了 measure 之后,我们可以简单看一下现在的绘制效果。

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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#C4E1A3"
android:orientation="vertical"
android:padding="30dp"
tools:context=".ui.pure.WrapContentActivity"
tools:ignore="HardcodedText">

<com.engineer.android.mini.ui.pure.MyFlowLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#9568E8">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="113331" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="124343431" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="113331" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="124343431" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="113331" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="124343431" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="1333351" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="1333351" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="1333351" />

</com.engineer.android.mini.ui.pure.MyFlowLayout>
</LinearLayout>

可以看到,宽度由于没有做特殊处理,就是父布局剩余的宽度。而高度差不多就是布局中这几个 TextView 的高度和 margin 值的和。由于 ViewGroup 默认的 onLayout 是空实现,因此,这里的子view 是没有执行 onLayout的,因此到此为止,子view 只是参与了 measure 过程,下面需要我们主动完成 layout 。

MyFlowLayout 之 measure

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
//存放容器中所有的View
private val mAllViews: ArrayList<List<View>> = ArrayList()

//存放每一行最高View的高度
private val mPerLineMaxHeight: ArrayList<Int> = ArrayList()

//每一行存放的 view
private var mLineViews = ArrayList<View>()

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
mAllViews.clear()
mPerLineMaxHeight.clear()

var lineWidth = 0 // 每一行已经占用的宽度
var lineMaxH = 0 // 每一行最大的高度

for (i in 0 until childCount) {
val childView = getChildAt(i)
val marginLp = childView.layoutParams as MarginLayoutParams
val childWidth = childView.measuredWidth + marginLp.leftMargin + marginLp.rightMargin
val childHeight = childView.measuredHeight + marginLp.topMargin + marginLp.bottomMargin

if(lineWidth + childWidth > width) { // 累计宽度超过一行的宽度
// 已累计的一行添加到列表中
mAllViews.add(mLineViews)
mPerLineMaxHeight.add(lineMaxH)
// 重置累计变量
lineWidth = 0
lineMaxH = 0
// 这里一定要创建新的,不能用 clean
mLineViews = ArrayList()
}
// 在同一行中的元素做累计
lineWidth += childWidth
lineMaxH = lineMaxH.coerceAtLeast(childHeight)
mLineViews.add(childView)
}
// 最后一行特殊处理
mAllViews.add(mLineViews)
mPerLineMaxHeight.add(lineMaxH)
// 遍历集合中的 view
var childLeft = 0
var childTop = 0
for ( i in 0 until mAllViews.size) {
val perLineViews = mAllViews[i] // 每一行的所有 views
val perLineH = mPerLineMaxHeight[i] // 每一行的最大高度
for (j in perLineViews.indices) {
val childView = perLineViews[j]
val lp = childView.layoutParams as MarginLayoutParams
val l = childLeft + lp.leftMargin
val t = childTop + lp.topMargin
val r = l + childView.measuredWidth
val b = t + childView.measuredHeight
Log.e("MyFlowLayout","l=$l,t=$t,r=$r,b=$b")
childView.layout(l,t,r,b)
childLeft += lp.leftMargin + childView.measuredWidth + lp.rightMargin
}
childLeft = 0
childTop += perLineH
}
}

这里的实现方式是用 list 记录每一行可以添加的 view ,然后再用一个 list 记录所有的行。
然后在布局子 view 的时候,就可以展开这个 list ,依次对内部的子view 做 layout 即可。完整源码参考CustomViewPlayGround

可以看到,经过 layout 布局每一个子view 之后,显示就正常了,内部的子view 按照标签布局的风格展示了。

总结

View 的 layout 相对 measure 和 draw 来说,需要自己实现的场景较少,即便需要实现,一般情况下也是比较简单的。但是 layout 自身的实现往往又不是独立的,很多时候要依赖 measure 阶段的一些辅助。比如 LinearLayout layout 阶段依赖 measure 阶段的总长度(总宽度)。

大部分场景下,需要重新 measure 的话,就需要 layout 。同时在 layout 执行 setFrame 的时候,也有可能触发
invalidate ,从而导致 draw 的调用。

加个鸡腿呗.