代碼是kotlin代碼蜈膨,所以看到有些值直接調(diào)用的不要疑惑,這些直接調(diào)用的值并不是類屬性實(shí)際也是調(diào)用的get和set方法
廢話不多說等舔,直接開始骚灸。
根據(jù)之前的文章,自定義ViewGroup需要重寫onMeasure和onLayout方法慌植,所以我們先來重寫onMeasure方法
重寫onMeasure方法分為下面幾步:
- 遍歷當(dāng)前ViewGroup的所有子View甚牲,調(diào)用
measureChildWithMargins
方法設(shè)置每個(gè)ChildView的寬高,measureChildWithMargins
和measureChild
的區(qū)別是measureChildWithMargins
同時(shí)設(shè)置了ChildView的margin值蝶柿,使用measureChildWithMargins
的前提是當(dāng)前ViewGroup覆蓋了generateLayoutParams
3個(gè)方法丈钙,下面我們會(huì)說到,沒有覆蓋generateLayoutParams
3個(gè)方法的話調(diào)用measureChildWithMargins
會(huì)崩潰交汤。 - 遍歷的時(shí)候同時(shí)記錄當(dāng)前ChildView的left和top值雏赦,在onLayout方法里使用這些值直接調(diào)用ChildView的layout方法,這樣就不需要在onLayout方法里再次遍歷計(jì)算ChildView的layout的值了芙扎,優(yōu)化代碼性能星岗。
- 遍歷完以后調(diào)用
View.resolveSize
方法傳入自己的寬高和寬高的MeasureSpec
,來計(jì)算自己在不同父容器的MeasureSpec
下的不同寬高
首先定義幾個(gè)類屬性
//記錄執(zhí)行onMeasure方法時(shí)ChildView左上角相對(duì)ViewGroup的坐標(biāo)
//這樣在onLayout方法就不需要再次計(jì)算了戒洼,提高效率
private val pointList = mutableListOf<Pair<Int, Int>>()
//行間距俏橘,在xml獲取該值
private var mRowSpacing = 0
//列間距,在xml獲取該值
private var mColumnSpacing = 0
重寫的onMeasure
方法如下
-
首先在該方法里我們先定義幾個(gè)變量:
需要1個(gè)變量記錄自己的寬(width)
需要1個(gè)變量記錄當(dāng)前行頂部坐標(biāo)(相對(duì)于ViewGroup)圈浇,用來最后計(jì)算該ViewGroup的高度(startY)
需要2個(gè)變量記錄當(dāng)前行的行寬和行高寥掐,用來判斷是否需要換行(lineWidth例获,lineHeight)
-
需要1個(gè)變量記錄當(dāng)前ChildView的左邊坐標(biāo)(相對(duì)于ViewGroup)(childLeft),用來保存當(dāng)前ChildView的繪制位置(left和top)
代碼如下//計(jì)算的寬度 var width = 0 //記錄當(dāng)前行頂部坐標(biāo)(相對(duì)于ViewGroup) var startY = 0 //記錄當(dāng)前行寬 var lineWidth = 0 //記錄當(dāng)前行高 var lineHeight = 0 //當(dāng)前ChildView的左邊坐標(biāo)(相對(duì)于ViewGroup) var childLeft = 0
-
然后我們還需要記錄2個(gè)值曹仗,因?yàn)镃hildView可用空間不包括上層容器的padding值榨汤,所以先定義2個(gè)值,下面會(huì)用到
//ViewGroup的左右padding值 val lrPaddingUsed = paddingLeft + paddingRight //ViewGroup的上下padding值 val tbPaddingUsed = paddingTop + paddingBottom
- 開始遍歷怎茫,并且記錄ChildView在調(diào)用ChildView自己的layout方法時(shí)需要的left和top值收壕,代碼如下
(0 until childCount).forEach { i ->
val child = getChildAt(i)
//GONE狀態(tài)的View就不需要執(zhí)行measureChild方法了,以提高效率轨蛤,因?yàn)檫@種狀態(tài)的View寬高是0(自定義View需要將GONE狀態(tài)的自己的寬高設(shè)置為0)
if (child.visibility != View.GONE) {
//2.調(diào)用measureChildWithMargins計(jì)算子View寬高
//因?yàn)橹貙懥?個(gè)generateLayout方法所以這里調(diào)用measureChildWithMargins不會(huì)有異常
measureChildWithMargins(child, widthMeasureSpec, lrPaddingUsed, heightMeasureSpec, tbPaddingUsed)
//3.1.子View執(zhí)行measure方法后該ViewGroup獲取子View的getMeasuredWidth和getMeasuredHeight
val layoutParams = child.layoutParams as MarginLayoutParams
//記錄該ChildView占用的空間
val childWidth = layoutParams.leftMargin + child.measuredWidth + layoutParams.rightMargin
val childHeight = layoutParams.topMargin + child.measuredHeight + layoutParams.bottomMargin
//3.2.計(jì)算ViewGroup自己的寬高
//第一個(gè)ChildView或者每行第一個(gè)ChildView的左邊都是沒有mColumnSpacing的
//每行的最后一個(gè)ChildView也是沒有mColumnSpacing的
//第一行第一個(gè)ChildView不需要換行
if (i == 0) {//第一行
//第一個(gè)ChildView初始化childLeft和childTop
//paddingLeft是ViewGroup的左邊內(nèi)間距
childLeft = paddingLeft + layoutParams.leftMargin
//paddingTop是ViewGroup的上邊內(nèi)間距蜜宪,將第一行的頂部坐標(biāo)設(shè)為paddingTop
startY = paddingTop
//lineWidth行寬在每行放置第一個(gè)ChildView時(shí)除了累加childWidth還需要累加ViewGroup的左右內(nèi)間距
lineWidth += lrPaddingUsed + childWidth
//lineHeight設(shè)置為第一行第一個(gè)ChildView的高度
lineHeight = childHeight
} else if (lineWidth + mColumnSpacing + childWidth <= measureWidth) {
//進(jìn)入該代碼塊代表當(dāng)前ChildView和上一個(gè)ChildView在同一行
//所以只需要設(shè)置childLeft而不需要設(shè)置childTop
//判斷時(shí)需要mColumnSpacing是因?yàn)?個(gè)ChildView之間有列間距
childLeft += mColumnSpacing + layoutParams.leftMargin
lineWidth += mColumnSpacing + childWidth
//lineHeight取最大高度
lineHeight = Math.max(childHeight, lineHeight)
} else {//需要換行,該ChildView放到了新行
//換行時(shí)childLeft和第一行一樣需要重新設(shè)置為ViewGroup的左邊內(nèi)間距加該ChildView的左外間距
childLeft = paddingLeft + layoutParams.leftMargin
//該行頂部坐標(biāo)(相對(duì)于ViewGroup)需要累加上一行的行高和行間距
startY += lineHeight + mRowSpacing
//下面2個(gè)值的操作和第一行一樣
lineWidth = lrPaddingUsed + childWidth
//lineHeight取新行第一個(gè)ChildView的高度
lineHeight = childHeight
}
//添加該ChildView的left和top到集合祥山,以便在該類的onLayout方法中調(diào)用ChildView的layout方法給該ChildView布局
pointList.add(Pair(childLeft, startY + layoutParams.topMargin))
//該ViewGroup的寬度取當(dāng)前該ViewGroup的寬度和行寬的最大值
width = Math.max(width, lineWidth)
//childLeft設(shè)置為該ChildView所占空間的右邊坐標(biāo)
//說明一下圃验,每個(gè)ChildView所占空間包括了它的margin值,因?yàn)镃hildView的外間距是不能顯示任何控件的缝呕,外間距這部分空間是View之間的間距
childLeft += child.measuredWidth + layoutParams.rightMargin
}
}
上面的代碼我把注釋寫的很詳細(xì)澳窑,已經(jīng)不需要解釋什么了。供常。摊聋。
-
遍歷完以后,就可以該ViewGroup的寬高也就可以確定了栈暇,接下來調(diào)用
View.resolveSize
方法來計(jì)算自己在不同父容器的MeasureSpec
下的不同寬高麻裁,代碼如下//4.調(diào)用resolveSize方法傳入自己計(jì)算的寬高和上級(jí)ViewGroup的MeasureSpec,得到自身不同MeasureSpec下的寬高 val resultWidth = resolveSize(width, widthMeasureSpec) val resultHeight = resolveSize(startY + lineHeight + paddingBottom, heightMeasureSpec)
-
然后調(diào)用
setMeasuredDimension
方法保存自己的寬高源祈,代碼如下//5.調(diào)用setMeasuredDimension保存自己的寬高 setMeasuredDimension(resultWidth, resultHeight)
到這里煎源,
onMeasure
方法就結(jié)束了,下面我們開始o(jì)nLayout方法的重寫
重寫的onLayout
方法如下
這個(gè)方法就很簡(jiǎn)單了香缺,因?yàn)樵趏nMeasure方法里已經(jīng)設(shè)置好了ChildView們的位置手销,讓我們來看一下代碼吧
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val childCount = this.childCount
(0 until childCount).forEach { i ->
val child = getChildAt(i)
//GONE狀態(tài)的View就不需要執(zhí)行l(wèi)ayout方法了,以提高效率赫悄,因?yàn)檫@種狀態(tài)的View寬高是0(自定義View需要將GONE狀態(tài)的自己的寬高設(shè)置為0)
if (child.visibility != View.GONE) {
val pair = pointList[i]
child.layout(pair.first, pair.second, pair.first + child.measuredWidth, pair.second + child.measuredHeight)
}
}
}
然后該流式布局的主要部分就完成了
重寫generateLayoutParams
的3個(gè)方法
然后我們需要重寫generateLayoutParams
的3個(gè)方法原献,否則在調(diào)用measureChildWithMargins
方法的時(shí)候是會(huì)報(bào)類轉(zhuǎn)換異常的馏慨,至于為什么自己看一下源碼就知道了埂淮,下面直接上重寫好的代碼
//ChildView的LayoutParams是包裹它的ViewGroup傳遞的,而默認(rèn)傳遞的ViewGroup.LayoutParams是沒有margin值的
//所以如果要使用margin需要重寫這3個(gè)方法写隶,ViewGroup會(huì)根據(jù)不同情況調(diào)用不同的方法的倔撞,所以最好把3個(gè)方法都重寫了
override fun generateDefaultLayoutParams(): LayoutParams {
return MarginLayoutParams(super.generateDefaultLayoutParams())
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
return MarginLayoutParams(p)
}
最后還有個(gè)之前提到的mRowSpacing
和mColumnSpacing
,這兩個(gè)值在xml里設(shè)置該流式布局的行間距和列間距慕趴,我們?cè)趓es/value下創(chuàng)建一個(gè)文件痪蝇,例如叫做attrs_flowlayout.xml
鄙陡,然后添加如下代碼
<resources>
<declare-styleable name="FlowLayout">
<attr name="rowSpacing" format="dimension"/>
<attr name="columnSpacing" format="dimension"/>
</declare-styleable>
</resources>
使用方法如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="liuhc.me.flowlayout.MainActivity">
<liuhc.me.flowlayout.FlowLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff0000"
app:rowSpacing="10dp"
app:columnSpacing="10dp"
android:padding="10dp">
...
</liuhc.me.flowlayout.FlowLayout>
</LinearLayout>
至此,一個(gè)流式布局就完成了躏啰,代碼提交到了github趁矾,地址:
https://github.com/ikakaxi/FlowLayout