Встроенные размеры в макетах 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 с текстом, нарисованным в одну строку.

Как работают встроенные функции

Вы можете создать компонент, который будет показывать на экране два текста, разделенных линией:

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

Для этого используйте 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
        )
        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 композиции 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 при его создании.

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