- ViewGroup是一个容器,内部可以包裹多个子View
- 自定义ViewGroup的时候也需要处理三个方法onMeasure,onLayout,onDraw方法
1. ViewGroup的onMeasure
- 因为ViewGroup是一个容器,onMeasure方法会更加复杂一些。因为ViewGroup在测量自己的宽高之前,需要先确定其内部子View所占大小,然后才能确定自己的大小。
比如如下一段代码:
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:orientation="vertical">
<TextView
android:layout_width="300dp"
android:layout_height="wrap_content"
android:background="#f00"
android:padding="10dp"
android:text="Text1" />
<TextView
android:layout_width="200dp"
android:layout_height="wrap_content"
android:background="#0f0"
android:padding="10dp"
android:text="Text2" />
<TextView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:background="#00f"
android:padding="10dp"
android:text="Text3" />
</LinearLayout>
- 效果:

- LinearLayout的宽高都是wrap_content表示由子控件的大小决定,而3个子控件的宽度分别是300,200,100,最终LinearLayout的宽度显示为300dp
- 所以自定义一个ViewGroup的时候,需要在onMeasure方法中综合考虑子View的宽度
自定义流式布局 FlowLayout
效果如下:

在实现FlowLayout中,FlowLayout中的每一行上的item个数不固定,当每行的item累计宽度超过可用总宽度,则需要重启一行排放item。所以需要在onMeasure方法中主动的分行计算出FlowLayout的最终高度。
/**
* 测量:先遍历计算子View的大小,然后最终计算得出ViewGroup的大小
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取到父类传递过来的宽高和测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//子View个数
int childCount = getChildCount();
//当前行计算的宽度
int lineWidth = 0;
//当前行最高的高度
int lineMaxHeight = 0;
//累加的高度
int totalHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//todo 先测量子View,再拿到子View的宽高\
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//判断一行子view累加宽度 是否超过 父容器规定的宽度
if (lineWidth + childWidth > widthSize) {
//超过最大宽度,另外起一行
lineWidth = childWidth;
//累加后的totalHeight 等于上面所有行的总高度,不包括当前行的高度
totalHeight += lineMaxHeight;
lineMaxHeight = childHeight;
} else {
//未超过最大宽度,宽度累加,高度选最大值
lineWidth += childWidth;
lineMaxHeight = Math.max(lineMaxHeight, childHeight);
}
//最后一个子view特殊处理,需要加上当前行的高度
if (i == childCount - 1) {
totalHeight += lineMaxHeight;
}
}
//设置最终的宽高
heightSize = heightMode == MeasureSpec.EXACTLY ? heightSize : totalHeight;
setMeasuredDimension(widthSize, heightSize);
}
- 解析
- 在自定义ViewGroup的onMeasure方法中,调用measureChild方法递归测量子View
- 通过叠加每一行的高度,计算出最终FlowLayout的最终高度totalHeight
2.onLayout
- 自定义ViewGroup时,复写onMeasure方法只是计算出ViewGroup的最终现实宽高,但是并没有规定其中包裹的子view显示在何处位置。要定义ViewGroup内部子View的显示规则,需要复写并实现onLayout方法
ViewGroup中的onLayout方法声明如下:
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
- VIewGroup的onLayout方法是一个抽象方法,自定义ViewGroup时必须主动实现如何排布子View,具体就是遍历每一个子view,调用childView.layout(l,t,r,b)方法来为每个子view设置具体的布局位置
- 四个参数分别代表左上右下的坐标位置。
/**
* 摆放子view
* 1。遍历拿到每一个childView的宽高,并计算每一行的宽高
* 2。二次遍历,调用childView的layout方法进行子view的摆放
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
allViews.clear();
lineMaxHeights.clear();
//保存每一行的子view
List<View> lineViews = new ArrayList<>();
//每一行的宽度
int lineTotalWidth = 0;
//每行最高的高度
int lineMaxHeight = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
// 获取到ChildView的宽高
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (lineTotalWidth + childWidth > getMeasuredWidth()) {
//超过一行宽度,将前一行的所有view添加到allViews,高度添加到集合中
allViews.add(lineViews);
lineMaxHeights.add(lineMaxHeight);
//开启新一行
lineMaxHeight = 0;
lineTotalWidth = 0;
lineViews = new ArrayList<>();
}
lineViews.add(childView);
lineTotalWidth += childWidth;
lineMaxHeight = Math.max(lineMaxHeight, childHeight);
}
//最后一行处理
allViews.add(lineViews);
lineMaxHeights.add(lineMaxHeight);
/**
* 遍历allViews,lineMaxHeights 拿到每一行的views和高度,进行子view进行摆放
*/
int mTop = 0;
int mLeft = 0;
for (int i = 0; i < allViews.size(); i++) {
List<View> oneLineViews = allViews.get(i);
Integer oneLineMaxHeight = lineMaxHeights.get(i);
//遍历每一行的views
for (int j = 0; j < oneLineViews.size(); j++) {
View childView = oneLineViews.get(j);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int left = mLeft + lp.leftMargin;
int top = mTop + lp.topMargin;
int right = left + childView.getMeasuredWidth();
int bottom = top + childView.getMeasuredHeight();
childView.layout(left, top, right, bottom);
mLeft += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
}
mLeft = 0;
mTop += oneLineMaxHeight;
}
}
- xml中使用
<com.timmy.demopractice.view.FlowLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff0">
...
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:background="#f00"
android:text="23452452"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:background="#f00"
android:text="8968796"
android:textSize="14sp" />
...
</com.timmy.demopractice.view.FlowLayout>
- 最终效果如下:

网友评论