ViewGroup绘制流程-测量和布局
绘制流程
分三步:
- onMeasure(): 测量当前控件的大小,在正式布局时提供建议(注意:只是建议,用不用要看onLayout函数)
- onLayout():对子控件进行布局
- onDraw():根据布局位置绘图
onMeasure()
void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
注意参数widthMeasureSpec和heightMeasureSpec,他们实际是MeasureSpec
MeasureSpec
父类对当前view的建议值,由Mode+Size组成
提取Mode和Size
Android提供了方法直接获取
利用MeasureSpec获取系统建议得到数值和模式
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
Mode分类
- UNSPECIFIED(未指定):父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小
- EXACTLY(完全):父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身的大小
- AT_MOST(至多):子元素至多达到指定的大小
Mode作用
基本规则:
- 不管父View是何模式,若子View有确切数值,则子View大小就是其本身大小,且mode是EXACTLY
- 若子View是match_parent,则模式与父View相同,且大小同父View(若父View是UNSPECIFIED,则子View大小为0)
- 若子View是wrap_content,则模式是AT_MOST,大小同父View,表示不可超过父View大小(若父View是UNSPECIFIED,则子View大小为0)
以下面XML举例(假设MyFlowLayout的父布局宽高都是match_parent):
<com.topband.viewstudy.demo_12.MyFlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
...
</com.topband.viewstudy.demo_12.MyFlowLayout>
我们宽度定义的是match_parent,对应的模式时EXACTLY,高度定义的是wrap_content,对应的模式时AT_MOST,因此我们在onMeasure()的时候可以直接使用宽度的值,但是我们高度是不确定的所以需要我们自己计算,最后通过setMeasuredDimension()保存计算结果
类似下面代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取父控件的宽高建议值
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureWidthSize = MeasureSpec.getSize(widthMeasureSpec);
int measureHightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureHightSize = MeasureSpec.getSize(heightMeasureSpec);
//测量逻辑
int width = 0;
int height = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
measureChild(child, widthMeasureSpec, heightMeasureSpec);
width = Math.max(childWidth, width);
height += childHeight + lp.topMargin + lp.bottomMargin;
}
//设置测量建议值
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidthSize : width,
(measureHightMode == MeasureSpec.EXACTLY) ? measureHightSize : height
);
}
onLayout()
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
top += lp.topMargin;
child.layout(lp.leftMargin, top, lp.leftMargin + childWidth, top + childHeight);
top += childHeight + lp.bottomMargin;
}
}
这个没啥好讲的, 就是通过layout()函数设置子控件的位置
注意点:
getMeasuredWidth是在measure()过程结束就可以获取到数值,getWidth()是在layout()过程结束后才能获取数值,一般是相同的,如果在layout()中传入的数值和getMeasuredWidth的数值不同,那么结果就不同了,也可以看出onMeasure()只是提供建议
onDraw()
根据布局位置绘图
实现一个FlowLayout
结合上面的知识点,就可以自定义一个类似FlowLayout效果了
先看效果:
1.测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//1 利用MeasureSpec获取系统建议得到数值和模式
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
//2 计算FlowLayout占用空间大小
int lineWidth = 0;//记录每一行的宽度
int lineHeight = 0;//记录每一行的高度
int width = 0;//记录整个FlowLayout宽度
int height = 0;//记录整个FlowLayout高度
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//测量子view
measureChild(child,widthMeasureSpec,heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//处理换行
if (lineWidth + childWidth > measureWidth) {
width = Math.max(lineWidth, childWidth);
height += lineHeight;
lineWidth = childWidth;
lineHeight = childHeight;
} else {
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
//处理最后一行
if (i == count - 1) {
width = Math.max(lineWidth, childWidth);
height += lineHeight;
}
}
//3 设置计算结果
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
2.布局
/**
* 布局所有子控件
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int lineWidth = 0;//行的宽
int lineHeight = 0;//行的高
int left = 0;
int top = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (childWidth + lineWidth > getMeasuredWidth()) {
left = 0;
top += lineHeight;
//换行后重新初始化
lineHeight = childHeight;
lineWidth = childWidth;
} else {
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
//子控件布局
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc, tc, rc, bc);
//移动到下一个子控件起点
left += childWidth;
}
}
3.注意点
获取margin需要重写
/***
* 提取margin值
* @param attrs
* @return
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
//使用时强转成MarginLayoutParams,就可以读取xml中的属性值了
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
完整代码
/**
* 作者: chenhao
* 创建日期: 2020-06-23
* 描述:自定义flowLaout
*/
public class MyFlowLayout extends ViewGroup {
public MyFlowLayout(Context context) {
super(context);
}
public MyFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//1 利用MeasureSpec获取系统建议得到数值和模式
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
//2 计算FlowLayout占用空间大小
int lineWidth = 0;//记录每一行的宽度
int lineHeight = 0;//记录每一行的高度
int width = 0;//记录整个FlowLayout宽度
int height = 0;//记录整个FlowLayout高度
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//测量子view
measureChild(child,widthMeasureSpec,heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//处理换行
if (lineWidth + childWidth > measureWidth) {
width = Math.max(lineWidth, childWidth);
height += lineHeight;
lineWidth = childWidth;
lineHeight = childHeight;
} else {
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
//处理最后一行
if (i == count - 1) {
width = Math.max(lineWidth, childWidth);
height += lineHeight;
}
}
//3 设置计算结果
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
/**
* 布局所有子控件
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int lineWidth = 0;//行的宽
int lineHeight = 0;//行的高
int left = 0;
int top = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (childWidth + lineWidth > getMeasuredWidth()) {
left = 0;
top += lineHeight;
//换行后重新初始化
lineHeight = childHeight;
lineWidth = childWidth;
} else {
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
//子控件布局
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc, tc, rc, bc);
//移动到下一个子控件起点
left += childWidth;
}
}
/***
* 提取margin值
* @param attrs
* @return
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
}