一、谷歌現有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的大小。