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

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