การวัดภายในในเลย์เอาต์ของ Compose

กฎข้อหนึ่งของ Compose คือคุณควรวัดองค์ประกอบย่อยเพียงครั้งเดียว การวัดองค์ประกอบย่อย 2 ครั้งจะทำให้เกิดข้อยกเว้นรันไทม์ อย่างไรก็ตาม บางครั้งคุณอาจต้องการข้อมูลบางอย่างเกี่ยวกับองค์ประกอบย่อยก่อนที่จะวัด

Intrinsics ช่วยให้คุณค้นหาองค์ประกอบย่อยก่อนที่จะวัดจริงได้

คุณสามารถขอ IntrinsicSize.Min หรือ IntrinsicSize.Max ของ Composable ได้ดังนี้

  • Modifier.width(IntrinsicSize.Min) - ความกว้างขั้นต่ำที่จำเป็นในการแสดงเนื้อหาอย่างเหมาะสมคือเท่าใด
  • Modifier.width(IntrinsicSize.Max) - ความกว้างสูงสุดที่คุณต้องการเพื่อแสดงเนื้อหาอย่างเหมาะสมคือเท่าใด
  • Modifier.height(IntrinsicSize.Min) - ความสูงขั้นต่ำที่จำเป็นในการแสดงเนื้อหาอย่างเหมาะสมคือเท่าใด
  • Modifier.height(IntrinsicSize.Max) - ความสูงสูงสุดที่คุณต้องการเพื่อแสดงเนื้อหาอย่างเหมาะสมคือเท่าใด

ตัวอย่างเช่น หากคุณขอ minIntrinsicHeight ของ Text ที่มีข้อจำกัด width แบบไม่จำกัดในเลย์เอาต์ที่กำหนดเอง ระบบจะแสดงผล height ของ Text โดยมีข้อความที่วาดในบรรทัดเดียว

การทำงานของ Intrinsics

คุณสามารถสร้าง Composable ที่แสดงข้อความ 2 รายการบนหน้าจอโดยคั่นด้วยตัวแบ่งได้ดังนี้

องค์ประกอบของข้อความ 2 รายการแสดงข้างกัน โดยมีเส้นแบ่งแนวตั้งคั่นกลาง

โดยใช้ Row กับ Composable Text 2 รายการที่เติมพื้นที่ว่าง และ 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
        )
        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 ขยายเต็มหน้าจอ ซึ่งไม่ใช่ลักษณะการทำงานที่ต้องการ

องค์ประกอบของข้อความ 2 รายการอยู่เคียงข้างกัน โดยมีตัวแบ่งอยู่ระหว่างองค์ประกอบ แต่ตัวแบ่งจะยืดลงไปด้านล่างของข้อความ

ซึ่งเกิดขึ้นเนื่องจาก Row วัดองค์ประกอบย่อยแต่ละรายการแยกกัน และไม่สามารถใช้ความสูงของ Text เพื่อจำกัด Divider ได้

หากต้องการให้ Divider เติมพื้นที่ว่างด้วยความสูงที่กำหนดแทน ให้ใช้ตัวแก้ไข height(IntrinsicSize.Min)

height(IntrinsicSize.Min) ปรับขนาดองค์ประกอบย่อยให้สูงเท่ากับความสูงภายในขั้นต่ำ เนื่องจากตัวแก้ไขนี้เป็นแบบเรียกซ้ำ จึงจะค้นหา minIntrinsicHeight ของ Row และรายการย่อย

การใช้ตัวแก้ไขนี้กับโค้ดจะทำให้โค้ดทำงานได้ตามที่คาดไว้

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

เมื่อมีตัวอย่าง

องค์ประกอบของข้อความ 2 รายการแสดงข้างกัน โดยมีเส้นแบ่งแนวตั้งคั่นกลาง

ความสูงของ Row จะกำหนดดังนี้

  • minIntrinsicHeight ของ Composable Row คือ minIntrinsicHeight สูงสุดขององค์ประกอบย่อย
  • minIntrinsicHeight ขององค์ประกอบ Divider คือ 0 เนื่องจากไม่ใช้พื้นที่หากไม่มีข้อจำกัด
  • Text minIntrinsicHeight คือข้อความสำหรับ width ที่เฉพาะเจาะจง
  • ดังนั้น ข้อจำกัด height ขององค์ประกอบ Row จึงกลายเป็นค่าสูงสุด minIntrinsicHeight ของ Text
  • จากนั้น Divider จะขยาย height ไปยังข้อจำกัด height ที่กำหนดโดย Row

Intrinsics ในเลย์เอาต์ที่กำหนดเอง

เมื่อสร้างตัวแก้ไข Layout หรือ layout ที่กำหนดเอง ระบบจะคำนวณการวัดค่าภายในโดยอัตโนมัติตามค่าประมาณ ดังนั้นการคำนวณจึงอาจไม่ถูกต้องสำหรับเลย์เอาต์บางแบบ API เหล่านี้มีตัวเลือกในการลบล้างค่าเริ่มต้นเหล่านี้

หากต้องการระบุการวัดค่าภายในของ Layout ที่กำหนดเอง ให้ลบล้าง minIntrinsicWidth, minIntrinsicHeight, maxIntrinsicWidth และ maxIntrinsicHeight ของอินเทอร์เฟซ MeasurePolicy เมื่อสร้าง การลบล้างต้องตรงกับสิ่งที่ measure ทำ ตัวอย่างเช่น ในคอลัมน์ที่กำหนดเองซึ่งวางองค์ประกอบย่อยซ้อนกันในแนวตั้งและกำหนดให้องค์ประกอบย่อยแต่ละรายการมีความกว้างเต็มคอลัมน์ ความสูงที่แท้จริงขั้นต่ำสำหรับ width หนึ่งๆ คือผลรวมของความสูงที่แท้จริงขั้นต่ำขององค์ประกอบย่อยสำหรับ width นั้นๆ

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

เมื่อสร้างตัวแก้ไข layout ที่กำหนดเอง ให้ลบล้างเมธอดที่เกี่ยวข้องในอินเทอร์เฟซ LayoutModifier การคำนวณภายในต้องตรงกับ measure ตัวอย่างเช่น สำหรับ height หนึ่งๆ ตัวแก้ไขที่เพิ่มระยะห่างจากขอบคงที่ในทุกด้านจะลบระยะห่างจากขอบแนวตั้งก่อนที่จะค้นหา minIntrinsicWidth ขององค์ประกอบย่อย และเพิ่มระยะห่างจากขอบแนวนอนลงในผลลัพธ์

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