iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Mobile Development

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

D09 / 為什麼我的按鈕這麼長? - Intrinsic measurements

今天大概會聊到的範圍

  • Intrinsic measurements

今天的標題可能會讓人有點疑惑,但這是我寫出這段 Code 時的第一反應。

Column(
        verticalArrangement = Arrangement.spacedBy(2.dp),
        modifier = Modifier
            .background(Color.White)
            .padding(4.dp)
    ) {

        Text(text = "Title", style = titleStyle)
        Text(text = "Looooooooooooooog Text")
        
        Spacer(modifier = Modifier.size(16.dp))
        
        Button(onClick = {}, modifier = Modifier.fillMaxWidth()) {
            Text(text = "GO")
        }
        
    }

這是一個 Dialog,在這個 Dialog 中,我希望整個 Dialog 和最寬的 Text 一樣寬,底部的 Button 可以是一個橫向、佔據整個底部的按鈕。直覺上,因為 Button 預期是最寬的狀態,所以我寫了 fillMaxWidth 。但我最後得到的畫面卻是這樣:

https://ithelp.ithome.com.tw/upload/images/20210923/20141597kAFnQs1OFD.png

按鈕就真的給我 fillMaxWidth 了,但因為沒有任何 constraint ,所以按鈕就一路延伸到和裝置的畫面一樣寬。

要解決這個問題前,要先了解 Compose 在測量 child 的大小時只會測量一次。就如同上一次聊到的一樣,每個 parent 在測量自己的大小前,都會先測量一次 child 的大小。都測量完之後,parent 才會知道自己多大。但這邊就出現了一個問題,因為測量 Button 時,parent 並不知道自己多大,也沒有特別的 constraint 傳給 Button,因此 Button 就回給 parent 自己要要和 max width 一樣大。

當然,我們可以在 parent ( Column 這一層 ) 寫死固定的 width 來設定 Dialog 大小,但這就沒辦法符合動態和最寬的文字一樣寬的需求。

Intrinsic measurements

這邊就要介紹到 Intrinsic measurements。Compose 的系統中設計了一個可以讓 parent 可以先知道 child 可能會多大。

每一個 Composable 都會有四個資料

IntrinsicMeasureScope.minIntrinsicWidth
IntrinsicMeasureScope.minIntrinsicHeight
IntrinsicMeasureScope.maxIntrinsicWidth
IntrinsicMeasureScope.maxIntrinsicHeight

( min | max )Intrinsic( Width | Height ) 都是在 InstrinsicMeasureScope 上,至於被使用的時機晚點會提到。因為在實際使用時,我們不會需要手動去取得這四個資料。而是透過 IntrinsicSize.(Max | Min) 來表示最大 or 最小

    Column(
        verticalArrangement = Arrangement.spacedBy(2.dp),
        modifier = Modifier
            .background(Color.White)
            .padding(4.dp)
            .width(IntrinsicSize.Max)  // 將 width 設定為測量前最大的 child 大小
    ) {
        Text(text = "Title", style = titleStyle)
        Text(text = "Looooooooooooooog Text")
        
        Spacer(modifier = Modifier.size(16.dp))
        
        Button(onClick = {}, modifier = Modifier.fillMaxWidth()) {
            Text(text = "GO")
        }        
    }

當你將 height/width 等設定成 IntrinsicSize.(Max | Min) 後,實際上會在 modifier chain 中增加一個 IntrinsicSizeModifier 。這個 Modifier 是一個 LayoutModifer,在計算 constraint 時會去參考 composable 提供的 intrinsic measurement。

// 設定 width(IntrinsicSize.Max) 時,就會在 modifier 中增加一個 MaxIntrinsicWidthModifier

private object MaxIntrinsicWidthModifier : IntrinsicSizeModifier {

    // modifier 中,會覆寫 constraint 的計算邏輯
    override fun MeasureScope.calculateContentConstraints(
        measurable: Measurable,
        constraints: Constraints
    ): Constraints {

        // 計算時,會參考 measurable 的 intrinsic size
        // 就是上面提到的 ( min | max )Intrinsic( Width | Height )
        val width = measurable.maxIntrinsicWidth(constraints.maxHeight)   
        return Constraints.fixedWidth(width)
    }
}

最後我們就可以產出一個以內容為大小依據的 Dialog,包含一個填滿 Dialog 的按鈕。

https://ithelp.ithome.com.tw/upload/images/20210923/20141597wIaVaPpTf4.png


今天實際的 code 其實不會很複雜,邏輯也很好理解。只是研究時發現和之前聊到的 Layout 邏輯整個串在一起,覺得很有趣。Compose 有很好的 render 邏輯,讓 measure 的次數降到最低。Intrinsic measurement 則是在這個結構下產生的好用工具。


Reference:


上一篇
D08 / 怎麼做自己的 Modifier.padding? - Custom Layout Modifier
下一篇
D10/ 我要怎麼把文字變美美的 - Text & AnnotatedString
系列文
認真學 Compose - 對 Jetpack Compose 的問題與探索30

尚未有邦友留言

立即登入留言