CustomView

image-20230306192616454

Measure

public void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

简述:测量是从ViewRootImpl调用performMeasure之后,由DecorView(Framelayout)的onMeasure开始,往子View一层一层递归调用其onMeasure测量宽高,如遇子View为ViewGroup,则分发测量给下一级直到非ViewGroup,最后再从最低层View确定宽高一层层返回确定上一级的宽高。

MeasureSpec

MeasureSpec 的完整英文是 Measure Specification测量规范,其中 Measure 表示测量,Specification 表示规范

MeasureSpec 就用来封装 View 的 sizemode 这两个属性,它是 View 的一个静态内部类,用一个 int 类型的三十二位整数来表示这两个属性,前两位表示 mode,后三十位表示 size。两个二进制位足够表示四种可能值,实际上 View 只用到了三种:UNSPECIFIED、EXACTLY、AT_MOST。makeMeasureSpec 方法就用于打包封装 size 和 mode 这两个属性值来生成 MeasureSpec

mode 含义
UNSPECIFIED(少见) ViewGroup 对于 View 没有任何限制,View 可以拿到任意想要的 size
EXACTLY View 本身设定了确切大小的 size。例如,View 的宽度设置为了 match_parent 或者具体的 dp 值,match_parent 即占满父容器,对于 View 来说也属于精准值
AT_MOST size 是 View 能够占据的最大空间,View 的最终大小不能超出该范围。对应 wrap_content,View 可以在父容器可以容纳的范围内申请空间

一般情况下,MeasureSpecMode 的取值由 View 的父容器决定,当父容器希望子 View 按照固定的大小来测量时,MeasureSpecMode 取 EXACTLY;当父容器希望子 View 不超过一定的大小时,MeasureSpecMode 取 AT_MOST。

UNSPECIFIED 模式相对较少用到,它表示 View 可以任意大小,一般用于一些特殊的场景,例如 ListView 中的子项,由于 ListView 的高度可以根据子项的数量动态调整,所以 ListView 在测量子项时,会将 MeasureSpecMode 设置为 UNSPECIFIED。当 MeasureSpecMode 为 UNSPECIFIED 时,MeasureSpecSize 的值通常是 0,但这并不影响 View 的测量,因为 View 可以根据自身的需要自由调整大小。

getChildMeasureSpec 方法是 View 的 MeasureSpec 是由其父容器 ViewGroup 的 MeasureSpec 和 View 自身的 LayoutParams 来共同决定的 这句话最直接的体现。(见附录代码getChildMeasureSpec

img

测量过程

对于 View 来说,其 MeasureSpec 是由其父容器 ViewGroup 的 MeasureSpec 和 View 自身的 LayoutParams 来共同决定的。此处所说的 View 也包含 ViewGroup 类型,因为父容器 ViewGroup 在测量 childView 的时候并不关心下一级的具体类型,而只是负责下发测量要求并接收测量结果,下一级如果是 View 类型那么就只需要测量自身并返回结果,下一级如果是 ViewGroup 类型那么就重复以上步骤并返回结果,整个视图树的绘制流程就通过这种层层递归调用的方式来完成测量,和 View 的事件分发机制非常相似

View通过重写 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法来返回自身的测量宽高结果,widthMeasureSpec、heightMeasureSpec这两个参数的值由 View 的父容器传递进来,因此它们实际上是父容器传递给子 View 的规格要求。在 onMeasure() 方法中,子 View 需要根据这两个参数计算出自己的测量宽度和测量高度,并将它们保存起来(setMeasuredDimension),以便后续的布局和绘制过程使用(getMeasureHeight/getMeasureWidth)。

Layout

protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

简述:继performMeasure执行完成之后,每个View有了自己的测量宽高(getMeasureWidth),接下来就由ViewRootImpl执行performLayout后走到DecorView的layout方法,决定自身的left,top,right,buttom等属性,定下来自身的位置。

View 的 layout 起始点也是从 ViewRootImpl 开始的,ViewRootImpl 的 performLayout 方法会调用 DecorView 的 layout 方法来启动 layout 流程,传入的后两个参数即屏幕的宽高大小

1
2
3
4
5
6
7
8
9
10
11
12
13
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
//...
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
//启动 layout 流程 host此时为DecorView
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
···
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}

layout(int l, int t, int r, int b) 是 View 类中的方法,传入的四个参数即我们熟知的 left、top、right、bottom,这四个值都是 View 相对父容器 ViewGroup 的坐标值。对于 DecorView 来说这四个值就分别是 0、0、screenWidth、screenHeight

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void layout(int l, int t, int r, int b) {
···
//重点
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);
···
}
···
}

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

setFrame 方法又会将 left、top、right、bottom 等四个值保存到 View 相应的几个全局变量上,至此 View 的 width 和 height 才真正确定下来,View 的 getWidth()getHeight()方法都是依靠这四个值做减法运算得到的。此外,这里也会回调 onSizeChanged 方法,在自定义 View 时我们往往就通过该方法来得到 View 的准确宽高大小,并在这里接收宽高大小变化的通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected boolean setFrame(int left, int top, int right, int bottom) {
···
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
···

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);
}
···
}
return changed;
}

e.g FrameLayout

详见代码(FrameLayout.onLayout)

layout 方法又会调用自身的 onLayout 方法。onLayout 方法在 View 类中是空实现,大部分情况下 View 都无需重写该方法。而 ViewGroup 又将其改为了抽象方法,即每个 ViewGroup 子类都需要通过实现该方法来管理自己的所有 childView 的摆放位置,FrameLayout 和 LinearLayout 等容器类就通过实现该方法来实现不同的布局效果

还是以 FrameLayout 为例子。FrameLayout 的布局特点就是会将所有的 childView 进行叠加覆盖显示,每个 childView 之间并不会形成相互约束,childView 主要是通过 layout_gravitylayout_margin 来声明自己在 FrameLayout 中的位置。FrameLayout 的 padding 也会占据一部分空间,从而影响 childView 的可用空间

FrameLayout 的 layoutChildren 方法就需要考虑以上因素,计算得出 childView 相对 FrameLayout 的 left、top、right、bottom 等值的大小,然后调用 childView 的 layout 方法,使得 childView 能够得到自己的真实宽高。如果 childView 也属于 ViewGroup 类型的话,就又会层层调用重复以上步骤完成整个视图树的 layout 操作

Draw

简述:继performMeasure、performLayout相继执行完成之后,每个View有了自己的确定宽高(getWidth)、位置(getLeft,这时候由ViewRootImpl的performDraw开始,走到DecordView的Draw开始,viewGroup dispatchDraw向下分发,View 操作canvas进行绘制。

draw 代表的是绘制视图的过程,在这个过程中 View 需要通过操作 Canvas 来实现自己 UI 效果

View 的 draw 起始点也是从 ViewRootImpl 开始的,ViewRootImpl 的 performDraw 方法会调用 drawSoftware 方法,再通过调用 DecorView 的 draw 方法来启动 draw 流程

1
2
3
4
5
6
7
8
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
// Draw with software renderer.
final Canvas canvas;
···
mView.draw(canvas);
···
}

View 的draw方法的重点看其调用的 onDrawdispatchDraw 这两个方法即可,这两个方法在 View 类中都是空实现

  • 对于自定义 View,我们需要重写onDraw方法来实现自己的特定 UI,无需关心dispatchDraw方法
  • 对于 ViewGroup,除了需要绘制背景色前景色等外,无需绘制自身,所以 ViewGroup 无需重写onDraw方法。而 dispatchDraw方法就是为 ViewGroup 准备的,用于向所有 childView 下发 draw 请求
1
2
3
4
5
6
7
8
9
	public void draw(Canvas canvas) {
···
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
···
}
复制代码

ViewGroup 的 dispatchDraw 方法会循环遍历所有 childView,使用同个 Canvas 对象来调用每个 childView 的 draw方法,层层调用完成整个视图树的绘制

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
    @Override
protected void dispatchDraw(Canvas canvas) {
···
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
//重点
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}

final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
//重点
more |= drawChild(canvas, child, drawingTime);
}
}
···
}

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
复制代码

附录

getChildMeasureSpec

spec 即 Parent 的 MeasureSpec

childDimension 是 子View(自身) 在布局文件中声明的尺寸值,>= 0时为像素值,-1为MATCH_PARENT,-2为WRAP_CONTENT

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
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
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);
}

FrameLayout.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
@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();
final int parentRight = right - left - getPaddingRightWithForeground();

final int parentTop = getPaddingTopWithForeground();
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;

//考虑水平方向上的约束条件
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);
}
}
}
Author

white crow

Posted on

2023-03-06

Updated on

2024-03-25

Licensed under