手把手教你自定義流式布局

代碼是kotlin代碼蜈膨,所以看到有些值直接調(diào)用的不要疑惑,這些直接調(diào)用的值并不是類屬性實(shí)際也是調(diào)用的get和set方法

廢話不多說等舔,直接開始骚灸。

根據(jù)之前的文章,自定義ViewGroup需要重寫onMeasure和onLayout方法慌植,所以我們先來重寫onMeasure方法

重寫onMeasure方法分為下面幾步:

  1. 遍歷當(dāng)前ViewGroup的所有子View甚牲,調(diào)用measureChildWithMargins方法設(shè)置每個(gè)ChildView的寬高,measureChildWithMarginsmeasureChild的區(qū)別是measureChildWithMargins同時(shí)設(shè)置了ChildView的margin值蝶柿,使用measureChildWithMargins的前提是當(dāng)前ViewGroup覆蓋了generateLayoutParams3個(gè)方法丈钙,下面我們會(huì)說到,沒有覆蓋generateLayoutParams3個(gè)方法的話調(diào)用measureChildWithMargins會(huì)崩潰交汤。
  2. 遍歷的時(shí)候同時(shí)記錄當(dāng)前ChildView的left和top值雏赦,在onLayout方法里使用這些值直接調(diào)用ChildView的layout方法,這樣就不需要在onLayout方法里再次遍歷計(jì)算ChildView的layout的值了芙扎,優(yōu)化代碼性能星岗。
  3. 遍歷完以后調(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方法如下

  1. 首先在該方法里我們先定義幾個(gè)變量:

    1. 需要1個(gè)變量記錄自己的寬(width)

    2. 需要1個(gè)變量記錄當(dāng)前行頂部坐標(biāo)(相對(duì)于ViewGroup)圈浇,用來最后計(jì)算該ViewGroup的高度(startY)

    3. 需要2個(gè)變量記錄當(dāng)前行的行寬和行高寥掐,用來判斷是否需要換行(lineWidth例获,lineHeight)

    4. 需要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
      
    5. 然后我們還需要記錄2個(gè)值曹仗,因?yàn)镃hildView可用空間不包括上層容器的padding值榨汤,所以先定義2個(gè)值,下面會(huì)用到

      //ViewGroup的左右padding值
      val lrPaddingUsed = paddingLeft + paddingRight
      //ViewGroup的上下padding值
      val tbPaddingUsed = paddingTop + paddingBottom
      
  1. 開始遍歷怎茫,并且記錄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)不需要解釋什么了。供常。摊聋。

  1. 遍歷完以后,就可以該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)
    
  2. 然后調(diào)用setMeasuredDimension方法保存自己的寬高源祈,代碼如下

    //5.調(diào)用setMeasuredDimension保存自己的寬高
    setMeasuredDimension(resultWidth, resultHeight)
    
  3. 到這里煎源,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è)之前提到的mRowSpacingmColumnSpacing,這兩個(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市给僵,隨后出現(xiàn)的幾起案子毫捣,更是在濱河造成了極大的恐慌,老刑警劉巖帝际,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蔓同,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蹲诀,警方通過查閱死者的電腦和手機(jī)斑粱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脯爪,“玉大人则北,你說我怎么就攤上這事『勐” “怎么了咒锻?”我有些...
    開封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)守屉。 經(jīng)常有香客問我惑艇,道長(zhǎng),這世上最難降的妖魔是什么拇泛? 我笑而不...
    開封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任滨巴,我火速辦了婚禮,結(jié)果婚禮上俺叭,老公的妹妹穿的比我還像新娘恭取。我一直安慰自己,他們只是感情好熄守,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開白布蜈垮。 她就那樣靜靜地躺著,像睡著了一般裕照。 火紅的嫁衣襯著肌膚如雪攒发。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天晋南,我揣著相機(jī)與錄音惠猿,去河邊找鬼。 笑死负间,一個(gè)胖子當(dāng)著我的面吹牛偶妖,可吹牛的內(nèi)容都是我干的姜凄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼趾访,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼态秧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起扼鞋,我...
    開封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤屿聋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后藏鹊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體润讥,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年盘寡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了楚殿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡竿痰,死狀恐怖脆粥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情影涉,我是刑警寧澤变隔,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站蟹倾,受9級(jí)特大地震影響匣缘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鲜棠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一肌厨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧豁陆,春花似錦柑爸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至祥诽,卻和暖如春譬圣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背原押。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工胁镐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盯漂。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像笨农,于是被迫代替她去往敵國(guó)和親就缆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

推薦閱讀更多精彩內(nèi)容