一、谷歌现有FlexboxLayout的效果
二、自定义实现一个简单的版本 MyFlowView(支持margin)
- 效果
- 直接贴源码
package com.haiheng.myapplication
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.ViewGroup
class MyFlowView(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
val TAG = "MyFlowView"
/**
* 存所有的子View,每一个元素 就是一行
*
*/
var childListView = mutableListOf<List<View>>()
/**
* 装一行
*/
var childLineListView = mutableListOf<View>()
/**
* 每一行的高
*/
var lineHeights = mutableListOf<Int>()
/**
* 父亲给我的宽高
*/
var selWidth = 0
var selHeight = 0
/**
* 测量
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
selWidth = MeasureSpec.getSize(widthMeasureSpec)
selHeight = MeasureSpec.getSize(heightMeasureSpec)
childListView = mutableListOf<List<View>>()
childLineListView = mutableListOf<View>()
lineHeights = mutableListOf<Int>()
/**
* 孩子希望的宽高
*/
var childNeedWidth = 0
var childNeedHeight = 0
/**
* 最终确定的宽高
*/
var finalWidth = 0
var finalHeight = 0
//当前行的宽度
var currentLineWidth = 0
//1、获取所有的子View
for (i in 0..(childCount - 1)) {
val childView = getChildAt(i)
if (childView.visibility != GONE) {
//2、获取子view的测量规格
val layoutParams = childView.layoutParams as MarginLayoutParams
val childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight,
layoutParams.width
)
val childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
paddingTop + paddingBottom,
layoutParams.height
)
/**
* 测量子View
*/
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
/**
* 当前行的宽
*/
Log.e(TAG, "父亲给的宽度${selWidth} and 当前孩子的宽度:${childView.measuredWidth}")
currentLineWidth =
currentLineWidth + childView.measuredWidth + layoutParams.leftMargin + layoutParams.rightMargin
//未换行
if (currentLineWidth < selWidth) {
Log.e(TAG, "currentLineWidth = ${currentLineWidth}")
//装在一行里面
childLineListView.add(childView)
}
//换行
else {
//装入当前行
childListView.add(childLineListView)
childLineListView = mutableListOf<View>()
childLineListView.add(childView)
currentLineWidth = childView.measuredWidth
}
if (i == (childCount - 1)) {
//如果是最后一行
childListView.add(childLineListView)
}
}
}
childNeedWidth = getChildNeeddWidth()
childNeedHeight = getChildNeeddHeight()
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
finalWidth = selWidth
} else {
finalWidth = childNeedWidth
}
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
finalHeight = selHeight
} else {
finalHeight = childNeedHeight
}
setMeasuredDimension(finalWidth, finalHeight)
}
/**
* 获取孩子需要的宽
*/
private fun getChildNeeddWidth(): Int {
val lineList = mutableListOf<Int>()
childListView.forEach {
// 获取当前行的和
val lineWidth = getLineWith(it)
lineList.add(lineWidth)
}
//获取所有行最长的
var width = 0
lineList.forEach {
if (it > width) {
width = it
}
}
return width
}
/**
* 获取当前行的和
*/
private fun getLineWith(it: List<View>): Int {
var width = 0
it.forEach {
width = width + it.measuredWidth
}
return width
}
/**
* 获取孩子需要的高
*/
private fun getChildNeeddHeight(): Int {
var height = 0
childListView.forEach {
height = height + getMaxHeight(it)
lineHeights.add(getMaxHeight(it))
}
return height
}
/**
* 获取当前行最高的
*/
private fun getMaxHeight(it: List<View>): Int {
var maxHeight = 0
it.forEach {
val layoutParams = it.layoutParams as MarginLayoutParams
val marginHeight = layoutParams.topMargin + layoutParams.bottomMargin
if ((it.measuredHeight + marginHeight) > maxHeight) {
maxHeight = it.measuredHeight + marginHeight
}
}
return maxHeight;
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//布局
var cLeft = paddingLeft
var cTop = paddingTop
for (i in 0..(childListView.size - 1)) {
/**
* 当前行的所有view
*/
val lineViews = childListView.get(i)
/**
* 当前行的高
*/
val lineHight = lineHeights.get(i)
lineViews.forEach { view ->
val layoutParams = view.layoutParams as MarginLayoutParams
val left = cLeft + layoutParams.leftMargin
val top = cTop + layoutParams.topMargin
val right = view.measuredWidth + left
val bottom = view.measuredHeight + top
view.layout(left, top, right, bottom)
cLeft = right + layoutParams.rightMargin
}
cLeft = paddingLeft
cTop = lineHight + cTop
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams? {
return MarginLayoutParams(context, attrs)
}
}
<?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:orientation="vertical"
tools:context=".MainActivity">
<com.haiheng.myapplication.MyFlowView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="水果味孕妇奶粉" />
<TextView
android:layout_marginBottom="20dp"
android:layout_marginTop="20dp"
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="儿童洗衣机" />
<TextView
android:layout_marginTop="40dp"
android:layout_marginBottom="50dp"
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="洗衣机全自动" />
<TextView
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="小度" />
<TextView
android:layout_marginTop="60dp"
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="儿童汽车可坐人" />
<TextView
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="抽真空收纳袋" />
<TextView
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="儿童滑板车" />
<TextView
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="稳压器 电容" />
<TextView
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="羊奶粉" />
<TextView
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="奶粉1段" />
<TextView
android:layout_marginTop="50dp"
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="图书勋章日" />
</com.haiheng.myapplication.MyFlowView>
</LinearLayout>
三、总结
- 自定ViewGroup通常实现onLayout、onMearsue 方法即可,因为自定义的是容器,不需要绘制。
- onMeasure可能由于父亲的调用多次,触发被多次调用,所以我们保存数据的成员变量,记得在onMeasure清空,用最后一次测量的为准。
- 首先要确定我们自定义ViewGroup的大小,而一个ViewGroup的大小由自身的MeasureSpec和所有的子View的大小决定,而子View的大小由自身的LayoutParams(布局属性)和父亲的MeasureSpec,padding 决定
- 所以第一步要根据父亲的MeasureSpec,padding 测量所有的子View大小,使用一个集合装起来
//2、获取子view的测量规格
val layoutParams = childView.layoutParams as MarginLayoutParams
val childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight,
layoutParams.width
)
val childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
paddingTop + paddingBottom,
layoutParams.height
)
/**
* 测量子View
*/
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
- 拿到所有子View大小之后,根据自身的MeasureSpec,计算出最终ViewGroup的大小,然后设置到自身
int realWidth = (widthMode == MeasureSpec.EXACTLY) ? selfWidth: parentNeededWidth;
int realHeight = (heightMode == MeasureSpec.EXACTLY) ?selfHeight: parentNeededHeight;
setMeasuredDimension(realWidth, realHeight);
- 测量之后就要布局,这个时候可以根据我们自定义ViewGroup算法,把我们的子View放到合适的座标位置
//布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int lineCount = allLines.size();
int curL = getPaddingLeft();
int curT = getPaddingTop();
for (int i = 0; i < lineCount; i++){
List<View> lineViews = allLines.get(i);
int lineHeight = lineHeights.get(i);
for (int j = 0; j < lineViews.size(); j++){
View view = lineViews.get(j);
int left = curL;
int top = curT;
// int right = left + view.getWidth();
// int bottom = top + view.getHeight();
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left,top,right,bottom);
curL = right + mHorizontalSpacing;
}
curT = curT + lineHeight + mVerticalSpacing;
curL = getPaddingLeft();
}
}
- 使用Margin的时候注意
(1)在自定义View类中重写generateLayoutParams方法
(2)使用view.layoutParams方法拿到margin
val layoutParams = view.layoutParams as MarginLayoutParams
val left = cLeft + layoutParams.leftMargin
val top = cTop + layoutParams.topMargin
(3)使用margin的时候,我们子View的具体大小不变,但是在设置自定义ViewGroup的大小的时候,记得加上margin的大小,并且在布局的时候,也需要考虑margin的大小。