Pengukuran intrinsik di tata letak Compose

Salah satu aturan Compose adalah Anda seharusnya hanya mengukur turunan satu kali; mengukur turunan dua kali akan memunculkan pengecualian runtime. Namun, ada kalanya Anda memerlukan beberapa informasi tentang turunan Anda sebelum mengukurnya.

Intrinsik memungkinkan Anda membuat kueri turunan sebelum benar-benar diukur.

Ke composable, Anda dapat meminta IntrinsicSize.Min atau IntrinsicSize.Max:

  • Modifier.width(IntrinsicSize.Min) - Berapa lebar minimum yang Anda perlukan untuk menampilkan konten dengan benar?
  • Modifier.width(IntrinsicSize.Max) - Berapa lebar maksimum yang Anda perlukan untuk menampilkan konten dengan benar?
  • Modifier.height(IntrinsicSize.Min) - Berapa tinggi minimum yang Anda perlukan untuk menampilkan konten dengan benar?
  • Modifier.height(IntrinsicSize.Max) - Berapa tinggi maksimum yang Anda perlukan untuk menampilkan konten dengan benar?

Misalnya, jika Anda meminta minIntrinsicHeight dari Text dengan batasan width yang tidak terbatas dalam tata letak kustom, variabel ini akan menampilkan height dari Text dengan teks yang digambar dalam satu baris.

Cara kerja intrinsik

Anda dapat membuat composable yang menampilkan dua teks di layar yang dipisahkan oleh pemisah:

Dua elemen teks berdampingan, dengan pembatas vertikal di antara elemen tersebut

Untuk melakukannya, gunakan Row dengan dua composable Text yang mengisi ruang yang tersedia, dan Divider di tengah. Divider harus setinggi Text tertinggi, dan harus tipis (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
        )
        VerticalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

Divider meluas ke seluruh layar, yang bukan perilaku yang diinginkan:

Dua elemen teks berdampingan, dengan pemisah di antara keduanya, tetapi pemisah membentang di bawah bagian bawah teks

Hal ini terjadi karena Row mengukur setiap turunan secara individual, dan tinggi Text tidak dapat digunakan untuk membatasi Divider.

Agar Divider mengisi ruang yang tersedia dengan tinggi tertentu, gunakan pengubah height(IntrinsicSize.Min).

height(IntrinsicSize.Min) mengukur ukuran turunannya agar setinggi tinggi intrinsik minimumnya. Karena pengubah ini bersifat berulang, pengubah ini akan mengkueri minIntrinsicHeight dari Row dan turunannya.

Menerapkan pengubah ini ke kode Anda akan membuatnya berfungsi seperti yang diharapkan:

@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
        )
        VerticalDivider(
            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")
        }
    }
}

Dengan pratinjau:

Dua elemen teks berdampingan, dengan pembatas vertikal di antara elemen tersebut

Tinggi Row ditentukan sebagai berikut:

  • minIntrinsicHeight composable Row adalah minIntrinsicHeight maksimum turunannya.
  • minIntrinsicHeight elemen Divider adalah 0, karena tidak menempati ruang jika tidak ada batasan yang ditentukan.
  • Text minIntrinsicHeight adalah teks untuk width tertentu.
  • Oleh karena itu, batasan height elemen Row menjadi minIntrinsicHeight maksimum dari Text.
  • Divider kemudian memperluas height ke batasan height yang ditentukan oleh Row.

Intrinsik di tata letak kustom

Saat membuat pengubah Layout atau layout kustom, pengukuran intrinsik dihitung secara otomatis berdasarkan perkiraan. Oleh karena itu, penghitungan mungkin tidak tepat untuk semua tata letak. API ini menawarkan opsi untuk mengganti perilaku default tersebut.

Untuk menentukan pengukuran intrinsik Layout kustom Anda, ganti minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth, dan maxIntrinsicHeight dari antarmuka MeasurePolicy saat membuatnya. Penggantian Anda harus cocok dengan tindakan measure. Misalnya, dalam kolom kustom yang menumpuk turunannya secara vertikal dan memberikan lebar kolom penuh kepada setiap turunan, tinggi intrinsik minimum untuk width tertentu adalah jumlah tinggi intrinsik minimum turunannya untuk width tersebut:

@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 {
                val childConstraints = constraints.copy(minHeight = 0)
                val placeables = measurables.map { measurable ->
                    measurable.measure(childConstraints)
                }
                val width = constraints.constrainWidth(
                    placeables.maxOfOrNull { it.width } ?: 0
                )
                val height = constraints.constrainHeight(
                    placeables.sumOf { it.height }
                )
                return layout(width, height) {
                    var yPosition = 0
                    placeables.forEach { placeable ->
                        placeable.placeRelative(x = 0, y = yPosition)
                        yPosition += placeable.height
                    }
                }
            }

            override fun IntrinsicMeasureScope.minIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,
                width: Int
            ): Int {
                return measurables.sumOf { measurable ->
                    measurable.minIntrinsicHeight(width)
                }
            }

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

Saat membuat pengubah layout kustom, ganti metode terkait di antarmuka LayoutModifier. Penghitungan intrinsik harus cocok dengan measure. Misalnya, untuk height tertentu, pengubah yang menambahkan padding tetap di semua sisi akan mengurangi padding vertikal sebelum mengkueri minIntrinsicWidth turunan, dan menambahkan padding horizontal ke hasilnya:

fun Modifier.myCustomModifier(padding: Dp = 8.dp) =
    this then object : LayoutModifier {
        override fun MeasureScope.measure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
            val paddingPx = padding.roundToPx()
            val horizontal = paddingPx * 2
            val vertical = paddingPx * 2
            val placeable = measurable.measure(
                constraints.offset(-horizontal, -vertical)
            )
            val width = constraints.constrainWidth(placeable.width + horizontal)
            val height = constraints.constrainHeight(placeable.height + vertical)
            return layout(width, height) {
                placeable.place(paddingPx, paddingPx)
            }
        }

        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurable: IntrinsicMeasurable,
            height: Int
        ): Int {
            val paddingPx = padding.roundToPx()
            val horizontal = paddingPx * 2
            val vertical = paddingPx * 2
            val childHeight = if (height == Constraints.Infinity) {
                Constraints.Infinity
            } else {
                (height - vertical).coerceAtLeast(0)
            }
            return measurable.minIntrinsicWidth(childHeight) + horizontal
        }

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