Внутренние измерения в макетах Compose

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

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

Для компонуемого объекта вы можете запросить его intrinsicWidth или intrinsicHeight :

  • (min|max)IntrinsicWidth : учитывая эту ширину, какова минимальная/максимальная ширина, на которой вы можете правильно отобразить свой контент?
  • (min|max)IntrinsicHeight : Учитывая эту высоту, какова минимальная/максимальная высота, на которой вы можете правильно отрисовать свой контент?

Например, если вы запросите minIntrinsicHeight для Text с бесконечной height , он вернет 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
        )
        HorizontalDivider(
            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) определяет размеры своих дочерних элементов, которые должны быть такими же высокими, как их минимальный внутренний рост. Поскольку он рекурсивный, он будет запрашивать Row и его дочерние элементы minIntrinsicHeight .

Применив это к нашему коду, он будет работать так, как ожидалось:

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

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

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

minIntrinsicHeight составного элемента Row будет максимальным minIntrinsicHeight его дочерних элементов. minIntrinsicHeight элемента Divider равен 0, поскольку он не занимает места, если не заданы никакие ограничения; Text minIntrinsicHeight будет соответствовать тексту определенной width . Таким образом, ограничение height элемента Row будет максимальным minIntrinsicHeight для Text s. Затем 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.
}

{% дословно %} {% дословно %} {% дословно %} {% дословно %}