#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 }
Предполагается, что он переизмеряет компонент на основе размера другого компонента, но что на самом деле делает этот код, для меня загадка.
- Как работает
subcompose
функция? - В чем смысл
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
, но максимум из этих 2composables
? Мне нужен пример, чтобы получить самый длинный из двух составных элементов, а затем установить ширину короткого так же, как и длинного. Как я могу достичь этого с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
)
}
}
)
}
}
И полный исходный код здесь.