Встроенные размеры в макетах Compose

Одно из правил Compose заключается в том, что дочерние элементы можно измерять только один раз. Если измерить их дважды, возникнет исключение времени выполнения. Однако бывают случаи, когда вам нужна информация о ваших детях, прежде чем измерять их.

Внутренние размеры позволяют запрашивать дочерние элементы до того, как они будут фактически измерены.

Для composable-функции можно запросить IntrinsicSize.Min или IntrinsicSize.Max:

  • Modifier.width(IntrinsicSize.Min) – минимальная ширина, необходимая для корректного показа контента.
  • Modifier.width(IntrinsicSize.Max) – какая максимальная ширина нужна, чтобы контент отображался правильно?
  • Modifier.height(IntrinsicSize.Min) – минимальная высота, необходимая для правильного отображения контента.
  • Modifier.height(IntrinsicSize.Max) – максимальная высота, необходимая для корректного показа контента.

Например, если вы запросите minIntrinsicHeight Text с бесконечными ограничениями width в специальном макете, то получите height Text с текстом, нарисованным в одной строке.

Внутренние свойства в действии

Вы можете создать composable-функцию, которая отображает на экране два текста, разделенных разделителем:

Два текстовых элемента рядом, разделенные вертикальной линией

Для этого используйте Row с двумя composable-функциями 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
        )
        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 разворачивается на весь экран, что нежелательно:

Два текстовых элемента рядом друг с другом, между ними разделитель, но разделитель растягивается ниже нижней части текста

Это происходит потому, что 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")
        }
    }
}

С предварительным просмотром:

Два текстовых элемента рядом, разделенные вертикальной линией

Высота Row определяется следующим образом:

  • minIntrinsicHeight composable-функции Row – это максимальное значение minIntrinsicHeight ее дочерних элементов.
  • Свойство minIntrinsicHeight элемента Divider равно 0, поскольку он не занимает места, если не заданы ограничения.
  • Text minIntrinsicHeight – это цвет текста для определенного width.
  • Поэтому ограничение height элемента Row становится максимальным значением minIntrinsicHeight элементов Text.
  • Затем Divider расширяет свой height до ограничений height, заданных Row.

Внутренние размеры в пользовательских макетах

При создании пользовательского модификатора 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.
    }