Phép đo nội tại trong bố cục Compose

Compose có một quy tắc là bạn chỉ nên đo lường thành phần con một lần; việc đo lường thành phần con hai lần sẽ cho ra một ngoại lệ trong thời gian chạy. Tuy nhiên, có một số trường hợp bạn cần thêm thông tin về thành phần con trước khi đo lường.

Hàm nội tại (intrinsics) cho phép bạn truy vấn thành phần con trước khi thực sự đo lường.

Đối với thành phần kết hợp, bạn có thể yêu cầu intrinsicWidth hoặc intrinsicHeight:

  • (min|max)IntrinsicWidth: Với chiều cao này, bạn có thể vẽ nội dung với chiều rộng tối thiểu/tối đa bao nhiêu là phù hợp?
  • (min|max)IntrinsicHeight: Với chiều rộng này, bạn có thể vẽ nội dung với chiều cao tối thiểu/tối đa bao nhiêu là phù hợp?

Ví dụ: nếu bạn yêu cầu minIntrinsicHeight của một Textwidth vô hạn, thì thao tác này sẽ trả về height của Text như thể văn bản đó được vẽ trong một dòng duy nhất.

Hàm nội tại trong thực tế

Hãy tưởng tượng chúng ta muốn tạo một thành phần kết hợp có thể hiển thị hai đoạn văn bản trên màn hình được phân tách bằng một đường phân cách như sau:

Hai thành phần văn bản cạnh nhau, ở giữa có một đường phân cách dọc

Chúng ta có thể làm việc này bằng cách nào? Chúng ta có thể có một Row với hai Text bên trong mở rộng nhiều nhất có thể và một Divider ở giữa. Chúng ta muốn Đường phân cách (Divider) cao bằng Text cao nhất và hẹp (width = 1.dp)

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )

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

Nếu xem trước, chúng ta sẽ thấy Đường phân cách mở rộng ra toàn màn hình và đó không phải là điều chúng ta muốn:

Hai thành phần văn bản cạnh nhau, ở giữa có một đường phân cách, nhưng đường phân cách kéo giãn xuống phần dưới văn bản

Điều này xảy ra vì Row đo lường từng thành phần con riêng lẻ và không thể sử dụng chiều cao của Text để ràng buộc Divider. Chúng ta muốn Divider lấp đầy không gian hiện có với một chiều cao nhất định. Do đó, chúng ta có thể sử dụng phương thức sửa đổi height(IntrinsicSize.Min).

height(IntrinsicSize.Min) cho phép thành phần con cao hơn chiều cao nội tại tối thiểu. Vì có tính đệ quy, lượt truy vấn này sẽ truy vấn Row và cũng như minIntrinsicHeight của thành phần con.

Khi áp dụng mã đó, mã sẽ hoạt động như mong đợi:

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

Bản xem trước:

Hai thành phần văn bản cạnh nhau, ở giữa có một đường phân cách dọc

minIntrinsicHeight của thành phần kết hợp Row sẽ là minIntrinsicHeight tối đa của thành phần con. minIntrinsicHeight của phần tử Divider là 0 vì không chiếm dung lượng nếu không có điều kiện ràng buộc nào được đưa ra; Text minIntrinsicHeight sẽ bằng của văn bản đã cung cấp một width cụ thể. Do đó, điều kiện ràng buộc height của phần tử Row sẽ là minIntrinsicHeight tối đa của Text. Sau đó, Divider sẽ mở rộng height tới giới hạn height do Row đưa ra.

Hàm nội tại trong bố cục tuỳ chỉnh

Khi tạo một phương thức sửa đổi Layout hoặc layout tuỳ chỉnh, hàm đo lường nội tại sẽ được tự động tính toán dựa trên giá trị gần đúng. Do đó, cách tính có thể không chính xác cho tất cả bố cục. Các API này có các tuỳ chọn để ghi đè những giá trị mặc định như vậy.

Để chỉ định các phép đo hàm nội tại của Layout tuỳ chỉnh, hãy ghi đè minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidthmaxIntrinsicHeight của giao diện MeasurePolicy khi tạo.

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

            override fun IntrinsicMeasureScope.maxIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,
                width: Int
            ): Int {
                // Logic for calculating custom maxIntrinsicHeight here
            }

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

Khi tạo đối tượng sửa đổi layout tuỳ chỉnh, hãy ghi đè các phương thức có liên quan trong giao diện 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.
})