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.
}