Compose 版面配置中的內建函式測量資料

Compose 的其中一項規則是只能測量子項一次;如果測量子項兩次,系統將擲回執行階段例外狀況。不過有時候,您必須先掌握子項的某些相關資訊,才能進行測量。

內建函式可讓您在實際測量前查詢子項相關資訊。

如果是可組合項,您可以查詢其 intrinsicWidthintrinsicHeight

  • (min|max)IntrinsicWidth:依據這個寬度,您可以正確繪製內容的最小/最大寬度為何?
  • (min|max)IntrinsicHeight:依據這個高度,您可以正確繪製內容的最小/最大高度為何?

舉例來說,如果在 height 設為無限的情況下查詢 TextminIntrinsicHeight,系統會將文字視為繪製在單一行中並傳回 Textheight

內建函式實際使用狀況

假設我們要建立一個可組合項,在畫面上顯示兩個文字元素並以分隔線隔開,如下所示:

兩個文字元素並排顯示,中間有一個垂直的分隔線

我們該怎麼做?我們可以設定一個 Row,在當中加入兩個 Text 並盡可能擴大兩者之間的距離,然後在中間加入一個 Divider。我們將 Divider 的高度設為 Text 的最大高度,寬度則為細 (width = 1.dp)。

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        HorizontalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

在預覽畫面中,我們發現 Divider 會擴展至整個畫面,這並非我們想要的結果:

兩個文字元素並排顯示,中間有一個垂直的分隔線,一直延伸至文字底部以下

之所以會發生這種情況,原因是 Row 會個別測量每個子項,而 Text 的高度無法用於限制 Divider。我們想讓 Divider 填滿指定高度的可用空間。想達到這個目的,我們可以使用 height(IntrinsicSize.Min) 修飾符。

height(IntrinsicSize.Min) 會將子項的高度強制調整為內建函式的最低高度。由於該修飾符有遞迴性,因此會查詢 Row 和其子項的 minIntrinsicHeight

套用到我們的程式碼後,就能產生我們預期的結果:

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        HorizontalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

// @Preview
@Composable
fun TwoTextsPreview() {
    MaterialTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

預覽如下:

兩個文字元素並排顯示,中間有一個垂直的分隔線

Row 可組合項的 minIntrinsicHeight 將是其子項的 minIntrinsicHeight 上限。由於 Divider 元素的 minIntrinsicHeight 在沒有限制的情況下不會佔用空間,因此為 0,而在指定特定 width 的情況下,Text minIntrinsicHeight 將為文字的高度。因此,Row 元素的 height 限制將是 TextminIntrinsicHeight 上限。Divider 隨即會將其 height 擴展至 Row 指定的 height 限制。

自訂版面配置中的內建函式

建立自訂 Layoutlayout 修飾符時,系統會根據估計值自動計算內建函式測量結果。因此,這些計算結果的正確性會依版面配置而異。這些 API 會提供覆寫這些預設值的選項。

如要指定自訂 Layout 的內建函式測量資料,請在建立 MeasurePolicy 介面時覆寫 minIntrinsicWidthminIntrinsicHeightmaxIntrinsicWidthmaxIntrinsicHeight

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier,
        measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                // Measure and layout here
                // ...
            }

            override fun IntrinsicMeasureScope.minIntrinsicWidth(
                measurables: List<IntrinsicMeasurable>,
                height: Int
            ): Int {
                // Logic here
                // ...
            }

            // Other intrinsics related methods have a default value,
            // you can override only the methods that you need.
        }
    )
}

建立自訂 layout 修飾符時,請在 LayoutModifier 介面中覆寫相關方法。

fun Modifier.myCustomModifier(/* ... */) = this then object : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // Measure and layout here
        // ...
    }

    override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int {
        // Logic here
        // ...
    }

    // Other intrinsics related methods have a default value,
    // you can override only the methods that you need.
}