作者:FunnySaltyFish https://juejin.cn/post/7063451846861406245
總覽Jetpack Compose 中,單個可組合項被顯示出來,總體上經歷三個過程
Composition(組合) -> Layout(布局) -> Drawing(繪製) 。
其中Layout階段又存在兩個方面的內容:Measure(測量) 和 Place(擺放)
今天我們主要着眼於 Layout 階段,看看各個 Composable 是如何正確確定各自位置和大小的
Layout階段主要做三件事情:
為簡化說明,我們先給出一個簡單例子。該例子中,所有元素只需要遍歷一次。
如下圖的 SearchResult 微件,它的構成如下:

現在我們來看看Layout過程在這個例子中是什麼情況
Measure請求測量根布局,即 Row

Row 為了知道自己的大小,就得先知道自己的子微件有多大,於是請求 Image 和 Column 測量它們自己。對於Image,由於它內部沒有其他微件,所以它可以完成自身測量過程並返回相關位置指令

接下來是 Column,因為它內部有兩個 Text,於是請求子微件測量。而對於 Text,它們也會正確返回自己的大小和位置指令

這時 Column 大小和位置指令即可正確確定。最後,Row 內部所有測量完成,它可以正確獲得自己的大小和位置指令

測量階段到此結束,接下來就是正確的擺放位置了
Place完成測量後,微件就可以根據自身大小從上至下執行各子微件的位置指令,從而確定每個微件的正確位置。
現在我們把目光轉向 Composition 階段。大家平時寫微件,內部都是由很多更基本的微件組合而來的,而事實上,這些基本的微件還有更底層的組成部分。如果我們展開剛剛的那個例子,它就成了這個樣子

在這裡,所有的葉節點(即沒有子元素的節點)都是 Layout 這個微件。我們來看看這個微件吧
Layout Composable此微件的簽名如下:
@ComposableinlinefunLayout(content:@Composable()->Unit,modifier:Modifier=Modifier,measurePolicy:MeasurePolicy)我們先看看第三個參數,這是之前從未見過的東西;而它恰恰控制着如何確定微件大小以及它們的擺放策略。
那來寫個例子吧。我們現在自定義一個簡單的縱向布局,也就是低配版 Column
自定義布局 - 縱向布局寫個框架
funVerticalLayout(modifier:Modifier=Modifier,content:@Composable()->Unit){Layout(modifier=modifier,content=content){measurables:List<Measurable>,constrains:Constraints->}}Measurable 代表可測量的,其定義如下:
interfaceMeasurable:IntrinsicMeasurable{/***Measuresthelayoutwith[constraints],returninga[Placeable]layoutthathasitsnew*size.A[Measurable]canonlybemeasuredonceinsidealayoutpass.*/funmeasure(constraints:Constraints):Placeable}可以看到,這是個接口,唯一的方法 measure 返回 Placeable,接下來根據這個 Placeable 擺放位置。而參數 measurables 其實也就是傳入的子微件形成的列表
而 Constraints 則描述了微件的大小策略,它的部分定義摘錄如下:

舉個栗子,如果我們想讓這個微件想多大就多大(類似 match_parent),那我們可以這樣寫:

如果它是固定大小(比如長寬50),那就是這樣寫

接下來我們就先獲取 placeable 吧
valplaceables=measurables.map{it.measure(constrains)}在這個簡單的例子中,我們不對 measure 的過程進行過多干預,直接測完獲得有大小的可放置項。
接下來確定我們的 VerticalLayout 的寬、高。對於咱們的布局,它的寬應該容納的下最寬的孩子,高應該是所有孩子之和。於是得到以下代碼:
//寬度:最寬的一項valwidth=placeables.maxOf{it.width}//高度:所有子微件高度之和valheight=placeables.sumOf{it.height}最後,我們調用 layout 方法返回最終的測量結果。前兩個參數為自身的寬高,第三個 lambda 確定每個 Placeable 的位置
layout(width,height){vary=0placeables.forEach{it.placeRelative(0,y)y+=it.height}}這裡用到了 Placeable.placeRelative 方法,它能夠正確處理從右到左布局的鏡像轉換
一個簡單的 Column 就寫好了。試一下?
funrandomColor()=Color(Random.nextInt(255),Random.nextInt(255),Random.nextInt(255))@ComposablefunCustomLayoutTest(){VerticalLayout(){(1..5).forEach{Box(modifier=Modifier.size(40.dp).background(randomColor()))}}}
嗯,工作基本正常。
接下來我們實現一個更複雜一點的:簡易瀑布流
自定義布局—簡易瀑布流先把基本的框架擼出來,在這裡只實現縱向的,橫向同理
@ComposablefunWaterfallFlowLayout(modifier:Modifier=Modifier,content:@Composable()->Unit,columns:Int=2//橫向幾列){Layout(modifier=modifier,content=content,){measurables:List<Measurable>,constrains:Constraints->TODO()}}我們加入了參數columns用來指定有幾列。由於瀑布流寬度是確定的,所以我們需要手動指定寬度
valitemWidth=constrains.maxWidth/2valitemConstraints=constrains.copy(minWidth=itemWidth,maxWidth=itemWidth)valplaceables=measurables.map{it.measure(itemConstraints)}在這裡我們用新的 itemConstraints 對子微件的大小進行約束,固定了子微件的寬度。接下來就是擺放了。瀑布流的擺放方式其實就是看看當前哪一列最矮,就把當前微件擺到哪一列,不斷重複就行
代碼如下:
@ComposablefunWaterfallFlowLayout(modifier:Modifier=Modifier,columns:Int=2,//橫向幾列content:@Composable()->Unit){Layout(modifier=modifier,content=content,){measurables:List<Measurable>,constrains:Constraints->valitemWidth=constrains.maxWidth/columnsvalitemConstraints=constrains.copy(minWidth=itemWidth,maxWidth=itemWidth)valplaceables=measurables.map{it.measure(itemConstraints)}//記錄當前各列高度valheights=IntArray(columns)layout(width=constrains.maxWidth,height=constrains.maxHeight){placeables.forEach{placeable->valminIndex=heights.minIndex()placeable.placeRelative(itemWidth*minIndex,heights[minIndex])heights[minIndex]+=placeable.height}}}}這裡用到了一個自定義的拓展函數 minIndex,作用是尋找數組中最小項的索引值,代碼很簡單,如下:
funIntArray.minIndex():Int{vari=0varmin=Int.MAX_VALUEthis.forEachIndexed{index,e->if(e<min){min=ei=index}}returni}效果如下(設置列數為3):

https://github.com/FunnySaltyFish/JetpackComposeStudy/tree/master/app/src/main/java/com/funny/compose/study/ui/post_layout
~ END ~
推薦閱讀