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

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

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

Для составного объекта можно запросить его 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 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.
}

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