Внутренние измерения в макетах 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) задает размеры своим потомкам, которые должны быть такими же высокими, как их минимальная внутренняя высота. Поскольку это рекурсивно, он будет запрашивать 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
        )
        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")
        }
    }
}

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

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

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

Одно из правил 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) задает размеры своим потомкам, которые должны быть такими же высокими, как их минимальная внутренняя высота. Поскольку это рекурсивно, он будет запрашивать 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
        )
        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")
        }
    }
}

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

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

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