iT邦幫忙

2021 iThome 鐵人賽

DAY 8
0
Mobile Development

認真學 Compose - 對 Jetpack Compose 的問題與探索系列 第 8

D08 / 怎麼做自己的 Modifier.padding? - Custom Layout Modifier

  • 分享至 

  • xImage
  •  

今天大概會聊到的範圍

  • 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

這邊提到的 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 中,可以拿到 MeasurableConstraints 兩個參數。

這些都和之前聊的一樣:

  • Modifier.layout 會先取得一個 Measurable ,這是一個還不知道大小的 child (通常是正在 modify 的物件)
  • 同時會拿到一個 Constraints , 這個是前面各種運算後留下來,這個 layout 自己可以佔的空間
  • Measurable 在執行 measure() 後,可以知道大小,變成一個知道大小但是還沒擺放的 Placable
  • PlacablePlacementScope 中可以擺放
  • 擺放完所有的 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 都會被測量一次(而且只會量一次),測量時需要提供 constraintsmeasurable 知道自己最大可以到多大 。測量完後變成 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 納入考量。

  1. 首先,我們需要將 top padding 的 dp 轉成 px 來運作
  2. 內容物的邊界不需要改變。因此,我們用同一個 constraint 對他進行測量即可。
  3. 再來,我們的邊界需要向上( top ) 延伸。因此,在 MeasureScope.layout 這邊,提供 height 時要增加一段 top padding
  4. 因為是 top padding。因次,在 place 時,我們需要將物件往下移動 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 觀念全部串在一起,算是不錯的練習。


上一篇
D07 / 怎麼顯示大量資料 - Lazy composables ( LazyColumn & StickyHeader )
下一篇
D09 / 為什麼我的按鈕這麼長? - Intrinsic measurements
系列文
認真學 Compose - 對 Jetpack Compose 的問題與探索30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言