Jetpack Сочиняет, как работает субкомпозиция?

#android #android-jetpack-compose

Вопрос:

Как видно из официальных документов, существует макет с именем SubcomposeLayout, определяемый как

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

Возможные варианты использования:

Вам нужно знать ограничения, переданные родителем во время композиции, и вы не можете решить свой вариант использования только с помощью пользовательского макета или LayoutModifier. См. androidx.сочинение.основа.расположение.Коробка с ограничениями.

Вы хотите использовать размер одного ребенка при составлении второго ребенка.

Вы хотите лениво составлять свои предметы в зависимости от доступного размера. Например, у вас есть список из 100 элементов, и вместо того, чтобы составлять их все, вы составляете только те, которые видны в данный момент(скажем, 5 из них), и составляете следующие элементы при прокрутке компонента.

Я искал Stackoverflow по SubcomposeLayout ключевому слову, но ничего не смог найти об этом, создал этот пример кода, скопировал большую его часть из официального документа, чтобы протестировать и узнать, как он работает

 @Composable
private fun SampleContent() {

    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
    ) {
        SubComponent(
            mainContent = {
                Text(
                    "MainContent",
                    modifier = Modifier
                        .background(Color(0xffF44336))
                        .height(60.dp),
                    color = Color.White
                )
            },
            dependentContent = {
                val size = it

                println("🤔 Dependent size: $size")
                Column() {

                    Text(
                        "Dependent Content",
                        modifier = Modifier
                            .background(Color(0xff9C27B0)),
                        color = Color.White
                    )
                }
            }
        )

    }
}

@Composable
private fun SubComponent(
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (IntSize) -> Unit
) {

    SubcomposeLayout { constraints ->

        val mainPlaceables = subcompose(SlotsEnum.Main, mainContent).map {
            it.measure(constraints)

        }

        val maxSize = mainPlaceables.fold(IntSize.Zero) { currentMax, placeable ->
            IntSize(
                width = maxOf(currentMax.width, placeable.width),
                height = maxOf(currentMax.height, placeable.height)
            )
        }

        layout(maxSize.width, maxSize.height) {

            mainPlaceables.forEach { it.placeRelative(0, 0) }

            subcompose(SlotsEnum.Dependent) {
                dependentContent(maxSize)
            }.forEach {
                it.measure(constraints).placeRelative(0, 0)
            }

        }
    }
}

enum class SlotsEnum { Main, Dependent }
 

Предполагается, что он переизмеряет компонент на основе размера другого компонента, но что на самом деле делает этот код, для меня загадка.

  1. Как работает subcompose функция?
  2. В чем смысл slotId и можем ли мы каким-то образом получить slotId?

Описание subCompose функции

Выполняет субкомпозицию предоставленного контента с заданным идентификатором. Параметры: slotId — уникальный идентификатор, представляющий слот, в который мы входим. Если у вас есть фиксированная сумма или слоты, вы можете использовать перечисления в качестве идентификаторов слотов, или если у вас есть список элементов, возможно, может сработать индекс в списке или какой-либо другой уникальный ключ. Чтобы иметь возможность правильно сопоставлять содержимое между повторными измерениями, вы должны предоставить объект, который равен тому, который вы использовали во время предыдущего измерения. содержимое — составное содержимое, определяющее слот. Он может создавать несколько макетов, в этом случае возвращаемый список измеряемых объектов будет содержать несколько элементов.

Может ли кто-нибудь объяснить, что это значит или/и предоставить рабочий образец для SubcomposeLayout ?

Ответ №1:

Предполагается, что он переизмеряет компонент на основе другого размера компонента…

SubcomposeLayout не перемеряет. Это позволяет откладывать состав и измерение контента до тех пор, пока не будут известны его ограничения со стороны родителя и можно будет измерить его содержимое, результаты которого и могут быть переданы в качестве параметра отложенному контенту. В приведенном выше примере вычисляется максимальный размер создаваемого содержимого mainContent и передается ему в качестве параметра deferredContent . Затем он измеряет deferredContent и помещает и mainContent то, и deferredContent другое друг на друга.

Самый простой пример использования SubcomposeLayout BoxWithConstraints, который просто передает ограничения, полученные от своего родителя, непосредственно в его содержимое. Ограничения коробки неизвестны до тех пор, пока родитель не измерит братьев и сестер коробки, что происходит во время компоновки, поэтому композиция content откладывается до компоновки.

Аналогично, в приведенном выше примере maxSize значение of mainContent неизвестно до тех пор, пока не будет вычислен макет, так deferredContent называемый в макете один раз maxSize . Он всегда помещается deferredContent поверх mainContent , поэтому предполагается, что deferredContent maxSize он каким-то образом используется, чтобы избежать затемнения контента, создаваемого mainContent . Вероятно, не лучший дизайн для составного, но составное должно было быть иллюстративным, а не полезным само по себе.

Обратите внимание, что subcompose в блоке можно вызывать несколько раз layout . Это, например, то, что происходит в LazyRow . Это slotId позволяет SubcomposeLayout отслеживать и управлять композициями, созданными с помощью вызова subcompose . Например, если вы генерируете содержимое из массива, вам может потребоваться использовать индекс массива в качестве его slotId параметра, позволяющего SubcomposeLayout определить, к какому subcompose сгенерированному в последний раз следует использовать во время перекомпозиции. Также, если а slotid больше не используется, SubcomposeLayout будет утилизироваться его соответствующий состав.

Что касается того, куда slotId идет, это зависит от того, кто звонит SubcomposeLayout . Если содержимое нуждается в этом, передайте его в качестве параметра. Приведенный выше пример в этом не нуждается, так как slotId он всегда один и тот же deferredContent , поэтому ему никуда не нужно идти.

Комментарии:

1. Что, если бы я хотел использовать максимальный размер не mainComponent , но максимум из этих 2 composables ? Мне нужен пример, чтобы получить самый длинный из двух составных элементов, а затем установить ширину короткого так же, как и длинного. Как я могу достичь этого с SubcomposeLayout помощью ?

2. Обратите внимание, что субкомпозиция может быть вызвана несколько раз в блоке компоновки. как это можно вызвать? Когда я звоню, он выдает исключение и запрашивает новый идентификатор. И может ли он быть вызван только внутри layout блока? И имеет ли это какое-либо значение для mainComponent или dependentComponent для вызова subcompose внутри или за пределами layout блока?

3. Каждый вызов субкомпозиции должен иметь уникальный идентификатор. Он может быть вызван только внутри блока компоновки, потому что это вызов приемника LayoutScope.

4. Спасибо, я сделал выборку, основанную на вашем ответе, которая устанавливает самую длинную ширину родного брата для родителя и более короткую. Имеет ли эта реализация какие-либо накладные расходы на производительность, так как я перемеряю mainComponent , если она короче, чем dependentComponent ? Я также видел SubcomposeLayoutState , когда я его использую?

Ответ №2:

Я сделал образец на основе образца, предоставленного официальными документами и ответом @chuckj, но все еще не уверен, что это эффективный или правильный способ его реализации.

Он в основном измеряет длину компонента, устанавливает родительскую ширину, переизмеряет более короткий компонент с minimumWidth Constraint помощью и изменяет размер короткого, как видно на этом gif. Вот как whatsapp в основном масштабирует цитаты и длину сообщений.


введите описание изображения здесь

Оранжевые и розовые контейнеры-это столбцы, которые являются прямыми дочерними элементами DynamicWidthLayout , которые используются SubcomposeLayout для повторного измерения.

 @Composable
private fun DynamicWidthLayout(
    modifier: Modifier = Modifier,
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (IntSize) -> Unit
) {

    SubcomposeLayout(modifier = modifier) { constraints ->


        var mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent).map {
            it.measure(constraints)
        }

        var maxSize =
            mainPlaceables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
                IntSize(
                    width = maxOf(currentMax.width, placeable.width),
                    height = maxOf(currentMax.height, placeable.height)
                )
            }

        val dependentMeasurables: List<Measurable> = subcompose(SlotsEnum.Dependent) {
            // 🔥🔥 Send maxSize of mainComponent to
            // dependent composable in case it might be used
            dependentContent(maxSize)
        }

        val dependentPlaceables: List<Placeable> = dependentMeasurables
            .map { measurable: Measurable ->
                measurable.measure(Constraints(maxSize.width, constraints.maxWidth))
            }

        // Get maximum width of dependent composable
        val maxWidth = dependentPlaceables.maxOf { it.width }


        println("🔥 DynamicWidthLayout-> maxSize width: ${maxSize.width}, height: ${maxSize.height}")

        // If width of dependent composable is longer than main one, remeasure main one
        // with dependent composable's width using it as minimumWidthConstraint
        if (maxWidth > maxSize.width) {

            println("🚀 DynamicWidthLayout REMEASURE MAIN COMPONENT")

            // !!! 🔥🤔 CANNOT use SlotsEnum.Main here why?
            mainPlaceables = subcompose(2, mainContent).map {
                it.measure(Constraints(maxWidth, constraints.maxWidth))
            }
        }

        // Our final maxSize is longest width and total height of main and dependent composables
        maxSize = IntSize(
            maxSize.width.coerceAtLeast(maxWidth),
            maxSize.height   dependentPlaceables.maxOf { it.height }
        )


        layout(maxSize.width, maxSize.height) {

            // Place layouts
            mainPlaceables.forEach { it.placeRelative(0, 0) }
            dependentPlaceables.forEach {
                it.placeRelative(0, mainPlaceables.maxOf { it.height })
            }
        }
    }
}


enum class SlotsEnum { Main, Dependent }
 

Использование

 @Composable
private fun TutorialContent() {

    val density = LocalDensity.current.density

    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
    ) {


        var mainText by remember { mutableStateOf(TextFieldValue("Main Component")) }
        var dependentText by remember { mutableStateOf(TextFieldValue("Dependent Component")) }


        OutlinedTextField(
            modifier = Modifier
                .padding(horizontal = 8.dp)
                .fillMaxWidth(),
            value = mainText,
            label = { Text("Main") },
            placeholder = { Text("Set text to change main width") },
            onValueChange = { newValue: TextFieldValue ->
                mainText = newValue
            }
        )

        OutlinedTextField(
            modifier = Modifier
                .padding(horizontal = 8.dp)
                .fillMaxWidth(),
            value = dependentText,
            label = { Text("Dependent") },
            placeholder = { Text("Set text to change dependent width") },
            onValueChange = { newValue ->
                dependentText = newValue
            }
        )

        DynamicWidthLayout(
            modifier = Modifier
                .padding(8.dp)
                .background(Color.LightGray)
                .padding(8.dp),
            mainContent = {

                println("🍏 DynamicWidthLayout-> MainContent {} composed")

                Column(
                    modifier = Modifier
                        .background(orange400)
                        .padding(4.dp)
                ) {
                    Text(
                        text = mainText.text,
                        modifier = Modifier
                            .background(blue400)
                            .height(40.dp),
                        color = Color.White
                    )
                }
            },
            dependentContent = { size: IntSize ->


                // 🔥 Measure max width of main component in dp  retrieved
                // by subCompose of dependent component from IntSize
                val maxWidth = with(density) {
                    size.width / this
                }.dp

                println(
                    "🍎 DynamicWidthLayout-> DependentContent composed "  
                            "Dependent size: $size, "
                              "maxWidth: $maxWidth"
                )

                Column(
                    modifier = Modifier
                        .background(pink400)
                        .padding(4.dp)
                ) {

                    Text(
                        text = dependentText.text,
                        modifier = Modifier
                            .background(green400),
                        color = Color.White
                    )
                }
            }
        )
    }
}
 

И полный исходный код здесь.