[英]Jetpack Compose set sibling Composables' width to longest one dynamically with SubcomposeLayout
[英]Jetpack Compose how does SubcomposeLayout work?
在官方文档中可以看到有一个名为 SubcomposeLayout 的布局定义为
布局模拟允许在测量阶段对实际内容进行子组合,例如使用测量期间计算的值作为子元素组合的参数。
可能的用例:
您需要知道父级在组合过程中传递的约束,并且不能仅使用自定义 Layout 或 LayoutModifier 来解决您的用例。 请参阅 androidx.compose.foundation.layout.BoxWithConstraints。
你想在第二个孩子的合成中使用一个孩子的大小。
你想根据可用尺寸懒惰地组合你的项目。 例如,您有一个包含 100 个项目的列表,而不是组合所有项目,您只组合当前可见的项目(比如其中的 5 个),并在组件滚动时组合下一个项目。
我用SubcomposeLayout
关键字搜索 Stackoverflow 但找不到任何相关信息,创建了这个示例代码,从官方文档中复制了大部分代码,以测试和了解它是如何工作的
@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
组合 function 如何工作?slotId
有什么意义,我们可以通过某种方式获得 slotId 吗? subCompose 的描述subCompose
使用给定的 slotId 对提供的内容执行子组合。 参数:slotId - 唯一的 id,代表我们正在组合的插槽。 如果您有固定数量或插槽,您可以使用枚举作为插槽 ID,或者如果您有一个项目列表,列表中的索引或其他一些唯一键可以工作。 为了能够正确匹配重新测量之间的内容,您应该提供 object,它等于您在上一次测量期间使用的那个。 content - 定义插槽的可组合内容。 它可以发出多个布局,在这种情况下,返回的 Measurables 列表将包含多个元素。
有人可以解释它的含义或/并为SubcomposeLayout
提供工作示例吗?
它应该根据另一个组件尺寸重新测量一个组件......
SubcomposeLayout
不会重新测量。 它允许推迟内容的组成和测量,直到知道其来自其父项的约束并且可以测量其某些内容,其结果可以作为参数传递给推迟的内容。 上面的示例计算mainContent
生成的内容的最大大小,并将其作为参数传递给deferredContent
。 然后它测量deferredContent
并将mainContent
和deferredContent
放在彼此的顶部。
如何使用的最简单的例子SubcomposeLayout
是BoxWithConstraints就是只传递它从它的父直接接收到其内容的约束。 在布局期间发生的父级测量了框的兄弟姐妹之前,框的约束是未知的,因此content
的组合被推迟到布局。
同样,对于上面的示例, mainContent
的maxSize
直到布局才知道,因此一旦计算出maxSize
就会在布局中调用deferredContent
。 它总是将deferredContent
放在mainContent
之上,因此假定deferredContent
以某种方式使用maxSize
以避免模糊mainContent
生成的内容。 可能不是可组合的最佳设计,但可组合的目的是说明性而不是本身有用。
请注意,可以在layout
块中多次调用subcompose
。 例如,这就是LazyRow
发生的情况。 该slotId
允许SubcomposeLayout
跟踪和管理通过调用创建的成分subcompose
。 例如,如果你是从一个阵列产生的内容,你可能希望使用数组作为它的指数slotId
允许SubcomposeLayout
确定哪些subcompose
产生应重构期间被用来最后一次。 此外,如果一个slotid
不使用任何更多, SubcomposeLayout
将配置其相应的组合物。
而对于其中slotId
云,即至的来电SubcomposeLayout
。 如果内容需要它,请将其作为参数传递。 上面的例子不需要它,因为对于deferredContent
的slotId
总是相同的,所以它不需要去任何地方。
我根据官方文档提供的示例和@chuckj 的回答制作了一个示例,但仍然不确定这种实现方式是否有效或正确。
它基本上测量最长组件的父宽度设置为这个宽度,并重新测量较短的一个,使用约束,将 minimumWidth 设置为这个宽度,如在此 gif 中所见。 这就是 whatsapp 基本上缩放报价和消息长度的方式。
橙色和粉红色容器是 Columns,它们直接使用SubcomposeLayout
重新测量的DynamicWidthLayout
子代。
@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
)
}
}
)
}
}
完整的源代码在这里。
最近我需要使用几乎相同的 SubcomposeLayout。 我需要一个带有可组合拇指的 Slider,我需要它来获得它的宽度,这样我就可以设置轨道的开始和结束以及我从 BoxWithConstraints 获得的 Slider 的全宽。
enum class SlotsEnum {
Slider, Thumb
}
/**
* [SubcomposeLayout] that measure [thumb] size to set Slider's track start and track width.
* @param thumb thumb Composable
* @param slider Slider composable that contains **thumb** and **track** of this Slider.
*/
@Composable
private fun SliderComposeLayout(
modifier: Modifier = Modifier,
thumb: @Composable () -> Unit,
slider: @Composable (IntSize, Constraints) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val thumbPlaceable: Placeable = subcompose(SlotsEnum.Thumb, thumb).map {
it.measure(constraints)
}.first()
// Width and height of the thumb Composable
val thumbSize = IntSize(thumbPlaceable.width, thumbPlaceable.height)
// Whole Slider Composable
val sliderPlaceable: Placeable = subcompose(SlotsEnum.Slider) {
slider(thumbSize, constraints)
}.map {
it.measure(constraints)
}.first()
val sliderWidth = sliderPlaceable.width
val sliderHeight = sliderPlaceable.height
layout(sliderWidth, sliderHeight) {
sliderPlaceable.placeRelative(0, 0)
}
}
}
测量拇指并将其尺寸作为 IntSize 和约束发送到 Slider,并且只放置 Slider,因为拇指已经放在内部 Slider 中,放置在这里会创建两个拇指。
并将其用作
SliderComposeLayout(
modifier = modifier
.minimumTouchTargetSize()
.requiredSizeIn(
minWidth = ThumbRadius * 2,
minHeight = ThumbRadius * 2,
),
thumb = { thumb() }
) { thumbSize: IntSize, constraints: Constraints ->
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val width = constraints.maxWidth.toFloat()
val thumbRadiusInPx = (thumbSize.width / 2).toFloat()
// Start of the track used for measuring progress,
// it's line + radius of cap which is half of height of track
// to draw this on canvas starting point of line
// should be at trackStart + trackHeightInPx / 2 while drawing
val trackStart: Float
// End of the track that is used for measuring progress
val trackEnd: Float
val strokeRadius: Float
with(LocalDensity.current) {
strokeRadius = trackHeight.toPx() / 2
trackStart = thumbRadiusInPx.coerceAtLeast(strokeRadius)
trackEnd = width - trackStart
}
// Rest of the code
}
结果
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.