今天大概會聊到的範圍
- layout modifier
上一次討論到 Modifier 時,覺得自己其實對物件如何繪製到畫面上其實一知半解。今天打算繼續研究負責調整佈局的 Layout Modifier。
這次我們一樣講 padding
這個 Modifier,padding 是一個 LayoutModifier
。他做的事情是在物件與其他物件之間保留空間。如果 jetpack compose 沒有內建 padding
,而是要我們自己實作的話,可以怎麼辦呢?
首先,我們先將我們預想的 Preview 寫出來
@Preview
@Composable
fun `Preview Modifier pad`(){
Box() {
Text("test", modifier = Modifier.padTop(4.dp))
}
}
因為
padding
要寫四個方位,為了簡化今天的說明,先以 padding top 為範例。
當然啦,Modifier.padTop()
目前是無法 compile 的,我們需要替 Modifier 寫一個 extension function
fun Modifier.padTop(top: Dp = 0.dp) {}
這邊提到的 layout modifier 和之前提到的 LayoutModifier
不同,這邊提到的是 Modifier 的 layout()
function。Modifier.layout
這個 function 可以用來創造一個客製化的佈局(最終仍是產生一個 LayoutModifier
) 。
fun Modifier.padTop(top: Dp = 0.dp) = layout { measurable, constraints ->
// MeasureScope
}
讓我們來看看 layout 長什麼樣子
fun Modifier.layout(
measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this.then(
LayoutModifierImpl(
measureBlock = measure,
inspectorInfo = debugInspectorInfo {
name = "layout"
properties["measure"] = measure
}
)
)
layout
本身會將 this
與新產生的 LayoutModifier
透過 then
串接起來,因此我們不需要額外做串接。
另外,我們可以看到 layout
接受的參數是一個以 MeasureScope
為 receiver 的 lambda,這個 lambda 需要回傳 MeasureResult
。並且,在這個 lambda 中,可以拿到 Measurable
與 Constraints
兩個參數。
這些都和之前聊的一樣:
Modifier.layout
會先取得一個 Measurable
,這是一個還不知道大小的 child (通常是正在 modify 的物件)Constraints
, 這個是前面各種運算後留下來,這個 layout 自己可以佔的空間Measurable
在執行 measure()
後,可以知道大小,變成一個知道大小但是還沒擺放的 Placable
Placable
在 PlacementScope
中可以擺放Placeable
後,就可以知道自身的大小 ( MeasureResult
)了解這樣的概念後,我們就可以來創造自己的 layout modifier 了!
首先,我們先做完基本的流程,並且什麼都不做調整:
fun Modifier.padTop(top: Dp = 0.dp) = this.then(
layout { measurable, constraints ->
// MeasureScope
// 1.
val placeable = measurable.measure(constraints)
// 2.
layout(placeable.width, placeable.height) {
// 3.
// layout function 中,
placeable.placeRelative(0, 0)
}
}
)
第一步( 1. ),我們要測量 child 的大小。在 Compose 的系統中,每個 Child 都會被測量一次(而且只會量一次),測量時需要提供 constraints
讓 measurable
知道自己最大可以到多大 。測量完後變成 placeable
可以擺放。
第二步( 2. ),框出自己的大小。在 MeasureScope
中,又有另一個 layout function,需要提供這個 function 預期的長、寬。
第三部( 3. ),實際擺放。 MeasureScope.layout
裡是 PlacementScope
,也就是可以擺放 Placable
的地方。在這邊可以呼叫 Placable.place
( or placeRelative
) 來進行擺放。
最後 MeasureScope.layout
會回傳 MeasureResult
,也是 Modifier.layout
trailing lambda 所需要的 return 值。
到目前為止,我們應該可以寫出一個完全無用的 Modifier。接下來,我們要將 top padding 納入考量。
因此,調整後我們的 Modifier 會變成這樣
fun Modifier.padTop(top: Dp = 0.dp) = this.then(
layout { measurable, constraints ->
// MeasureScope
// 0.
val topPx = top.roundToPx()
// 1.
val placeable = measurable.measure(constraints)
// 2.
layout(placeable.width, placeable.height + topPx) {
// 3.
placeable.placeRelative(0, topPx)
}
}
)
我們順利的建立了一個客製化的 top padding modifier。但是假設今天我們的 Modifier 很特別,只在特殊的場景適用,不希望其他人在其他場景誤用的話,該怎麼辦呢?
這邊要使用講到之前說過的 Modifier Scope 概念。建立一個 Scope 並且 Modifier 只在這個範圍內能呼叫的到、能產生作用。
在那之前,我們先定義一個 Composable,作為我們限縮 Modifier 的 container。
@Composable
fun MyLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Surface(modifier = modifier) {
content()
}
}
很單純的 Composable,基本上只是將 content 包裝在 Surface 內而已。
接下來才是有趣的部分,我們需創造一個 Scope 來限縮 modifier。
interface MyScope
// ---
@Composable
fun MyLayout(
modifier: Modifier = Modifier,
content: @Composable MyScope.() -> Unit // << 改成以 MyScope 做 receiver
) {
將 Modifier 的定義搬進 MyScope 中:
interface MyScope {
fun Modifier.padTop(top: Dp = 0.dp): Modifier
}
因為是 interface,所以這邊還不會有實作。但是將 Modifier.padTop 放在 MyScope 中,我們就可以確保今天不是在 MyLayout 的 composable 都不會有這個 Modifier。
接下來,我們將 MyScope 實作,並且將 padTop 的實作也放進 MyScope 實作中。
object MyScopeInstance: MyScope {
override fun Modifier.padTop(top: Dp) = layout { measurable, constraints ->
// ...
}
}
最後,因為 MyLayout 的 content 需要一個 receiver。因此我們用 MyScopeInstance 作 receiver 呼叫他
@Composable
fun MyLayout(
modifier: Modifier = Modifier,
content: @Composable MyScope.() -> Unit
) {
Surface(modifier = modifier) {
MyScopeInstance.content() // << 改以 MyScopeInstance 呼叫
}
}
這次模仿實作一次,其實大多都是跟著官方文件實作的。但這個實作等於是將之前試圖了解的 Layout 流程與 Modifier Scope 觀念全部串在一起,算是不錯的練習。