如何在不重新组合的情况下获得精确的尺寸?

uni*_*pps 7 android kotlin android-jetpack-compose

我需要可组合项的大小来绘制动态线条,但我不想通过以下方式获取大小:

var size by remember { mutableStateOf(IntSize.Zero) }

Modifier.onSizeChanged{size = it}

or

Modifier.onGloballyPositioned{size = it.size}
Run Code Online (Sandbox Code Playgroud)

因为我不想重新组合。

目前我正在从 BoxWithConstraints 获取大小并作为参数传递,如下所示:

fun DrawLines(intSize:IntSize){
// handle lines
}

Run Code Online (Sandbox Code Playgroud)

有没有更好的方法或者这就是我现在能做的?

感谢帮助。

Thr*_*ian 25

如果您要使用可组合项的大小来绘制线条,您可以将函数更改为DrawScope返回可组合项大小的扩展。如果不是这种情况,请检查下面的答案。

fun DrawScope.drawLine() {
    this.size
}
Run Code Online (Sandbox Code Playgroud)

并在其中任一内部调用此函数

Modifier.drawBehind{}Modifier.drawWithContent{}或者Modifier.drawWithCache{}。如果您不想更改函数,也可以在这些修饰符中传递大小。

BoxWithConstraints 和 SubcomposeLayout

BoxWithConstraints获取内容的精确大小并不总是可靠的,顾名思义,它有利于获取Constraints. 我在这里Constraints有一个关于BoxConstraints 使用哪个大小修饰符返回什么的详细答案。您可以查看答案的约束部分来检查每个 的结果。Modifier

例如

BoxWithConstraints() {
    Text(
        modifier = Modifier
            .size(200.dp)
            .border(2.dp, Color.Red),
        text = "Constraints: ${constraints.minWidth}, max: ${constraints.maxWidth}"
    )
}
Run Code Online (Sandbox Code Playgroud)

将返回minWidth = 0pxmaxWidth 1080px(我的设备的宽度,以 px 为单位)而不是 525px,即我的设备中的 200.dp。

如果不重新组合,您就无法Layout单独获取维度,这就是BoxWithConstraints使用SubcomposeLayout传递Constraints给内容的原因。你可以看看这个问题来了解一下SubcomposeLayout

BoxWithConstraints 源代码

@Composable
@UiComposable
fun BoxWithConstraints(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content:
        @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
) {
    val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
    SubcomposeLayout(modifier) { constraints ->
        val scope = BoxWithConstraintsScopeImpl(this, constraints)
        val measurables = subcompose(Unit) { scope.content() }
        with(measurePolicy) { measure(measurables, constraints) }
    }
}
Run Code Online (Sandbox Code Playgroud)

SubcomposeLayout允许推迟内容的组成和测量,直到已知其来自其父级的约束并且可以测量其某些内容,并且可以将其结果作为参数传递给推迟的内容。

在下面的实现中,它可以根据需要进行自定义,我根据需要使用它的多个版本。

您可以根据您的需要自定义如何求和或最大宽度或高度、布局宽度或高度、如何放置项目以具有行、列或框等行为。您可以限制为一个或多个可组合项,具体取决于您。唯一需要做的就是将Size/IntSize/DpSize从一个可组合项传递到另一个可组合项。

/**
 * SubcomposeLayout that [SubcomposeMeasureScope.subcompose]s [mainContent]
 * and gets total size of [mainContent] and passes this size to [dependentContent].
 * This layout passes exact size of content unlike
 * BoxWithConstraints which returns [Constraints] that doesn't match Composable dimensions under
 * some circumstances
 *
 * @param placeMainContent when set to true places main content. Set this flag to false
 * when dimensions of content is required for inside [mainContent]. Just measure it then pass
 * its dimensions to any child composable
 *
 * @param mainContent Composable is used for calculating size and pass it
 * to Composables that depend on it
 *
 * @param dependentContent Composable requires dimensions of [mainContent] to set its size.
 * One example for this is overlay over Composable that should match [mainContent] size.
 *
 */
@Composable
fun DimensionSubcomposeLayout(
    modifier: Modifier = Modifier,
    placeMainContent: Boolean = true,
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (Size) -> Unit
) {
    SubcomposeLayout(
        modifier = modifier
    ) { constraints: Constraints ->

        // Subcompose(compose only a section) main content and get Placeable
        val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
            .map {
                it.measure(constraints.copy(minWidth = 0, minHeight = 0))
            }

        // Get max width and height of main component
        var maxWidth = 0
        var maxHeight = 0

        mainPlaceables.forEach { placeable: Placeable ->
            maxWidth += placeable.width
            maxHeight = placeable.height
        }

        val dependentPlaceables: List<Placeable> = subcompose(SlotsEnum.Dependent) {
            dependentContent(Size(maxWidth.toFloat(), maxHeight.toFloat()))
        }
            .map { measurable: Measurable ->
                measurable.measure(constraints)
            }


        layout(maxWidth, maxHeight) {

            if (placeMainContent) {
                mainPlaceables.forEach { placeable: Placeable ->
                    placeable.placeRelative(0, 0)
                }
            }

            dependentPlaceables.forEach { placeable: Placeable ->
                placeable.placeRelative(0, 0)
            }
        }
    }
}

enum class SlotsEnum { Main, Dependent }
Run Code Online (Sandbox Code Playgroud)

用法

val content = @Composable {
    Box(
        modifier = Modifier
            .size(200.dp)
            .background(Color.Red)
    )
}

val density = LocalDensity.current

DimensionSubcomposeLayout(
    mainContent = { content() },
    dependentContent = { size: Size ->
        content()
        val dpSize = density.run {size.toDpSize() }
        Box(Modifier.size(dpSize).border(3.dp, Color.Green))
    },
    placeMainContent = false
)
Run Code Online (Sandbox Code Playgroud)

或者

DimensionSubcomposeLayout(
    mainContent = { content() },
    dependentContent = { size: Size ->
        val dpSize = density.run {size.toDpSize() }
        Box(Modifier.size(dpSize).border(3.dp, Color.Green))
    }
)
Run Code Online (Sandbox Code Playgroud)

结果

在下面的示例中,我们根据红色背景的框设置绿色边框的框的大小。这对于初学者来说可能很复杂,但这就是您无需重新组合可组合项即可获得维度的方法。上面提供的链接中的 SubcomposeLayout 问题和答案可能会有所帮助。我发布了几个答案并链接了其他答案,展示了如何使用它。

在此输入图像描述

额外部分

布局、范围和约束同级

您可以以类似于 Box、Row、Column 的方式使用布局,使用接口、实现和更改此实现的属性将信息从内部传递到内容

interface DimensionScope {
    var size: Size
}


class DimensionScopeImpl(override var size: Size = Size.Zero) : DimensionScope
Run Code Online (Sandbox Code Playgroud)

并实现 DimensionScope 和 Layout。

@Composable
private fun DimensionLayout(
    modifier: Modifier = Modifier,
    content: @Composable DimensionScope.() -> Unit
) {
    val dimensionScope = remember{DimensionScopeImpl()}

    Layout(
        modifier = modifier,
     //  since we invoke it here it will have Size.Zero
     // on Composition then will have size value below
        content = { dimensionScope.content() }
    ) { measurables: List<Measurable>, constraints: Constraints ->

        val placeables = measurables.map { measurable: Measurable ->
            measurable.measure(constraints)
        }

        val maxWidth = placeables.maxOf { it.width }
        val maxHeight = placeables.maxOf { it.height }

        dimensionScope.size = Size(maxWidth.toFloat(), maxHeight.toFloat())

        layout(maxWidth, maxHeight) {
            placeables.forEach { placeable: Placeable ->
                placeable.placeRelative(0, 0)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

由于我们在能够测量之前调用,并且Layout只能测量一次,因此我们将无法在第一个组合上将正确的 Size 传递给 DimensionScopeImpl,正如我上面提到的。在下次重新组合时,因为我们记得DimensionScopeImpl我们得到了正确的尺寸,并且Text尺寸设置正确,并且我们看到Text了边框。

Column(modifier = Modifier.fillMaxSize().padding(20.dp)) {
    val density = LocalDensity.current

    var counter by remember { mutableStateOf(0) }

    DimensionLayout {
        Box(
            modifier = Modifier
                .size(200.dp)
                .background(Color.Red)
        )
        val dpSize = density.run { size.toDpSize() }

        Text(
            text = "counter: $counter", modifier = Modifier
                .size(dpSize)
                .border(3.dp, Color.Green)
        )
    }

    Button(onClick = { counter++ }) {
        Text("Counter")
    }

}
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

我们无法获得正确的尺寸,因为我们需要dimensionScope.content()在测量之前调用,但在某些情况下,您可能能够从父级或您的计算中获取约束、尺寸或参数。在这种情况下,您可以传递尺寸。我制作了一个基于 ContentScale 传递绘图区域的图像,正如您在此处使用范围看到的那样。

有选择地测量以将一个兄弟姐妹与另一个兄弟姐妹匹配

无法使用布局传递并不意味着我们不能将其他同级设置为相同的大小并在需要时使用其尺寸。

为了演示,我们将第二个可组合项的尺寸更改为第一个可组合项的尺寸

@Composable
private fun MatchDimensionsLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables: List<Measurable>, constraints: Constraints ->
        // For demonstration we will change dimensions of second Composable to firs ones
        require(measurables.size == 2)
        val firstMeasurable = measurables.first()
        val secondMeasurable = measurables.last()

        val firsPlaceable = firstMeasurable.measure(constraints)
        // Measure with first one's width and height
        val secondPlaceable =
            secondMeasurable.measure(Constraints.fixed(firsPlaceable.width, firsPlaceable.height))

        // Set width and height of this Composable width of first one, height total of first
        // and second

        val containerWidth = firsPlaceable.width
        val containerHeight = firsPlaceable.height + secondPlaceable.height

        layout(containerWidth, containerHeight) {
            firsPlaceable.placeRelative(0,0)
            val y = firsPlaceable.height
            secondPlaceable.placeRelative(0,y)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

示范

MatchDimensionsLayout {
    BoxWithConstraints {
        Text(
            modifier = Modifier
                .size(200.dp)
                .border(2.dp, Color.Red),
            text = "Constraints: ${constraints.minWidth}\n" +
                    "max: ${constraints.maxWidth}"
        )
    }
    BoxWithConstraints {
        Text(
            modifier = Modifier
                .size(400.dp)
                .border(2.dp, Color.Red),
            text = "Constraints: ${constraints.minWidth}\n" +
                    "max: ${constraints.maxWidth}"
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

由于我们使用 Constraints.fixed 将第二个尺寸与第一个尺寸相匹配,因此测量 BoxWithConstraints 现在会返回第一个或主可组合项的尺寸,即使我们无法将尺寸作为Layout参数传递。

您还可以使用Modifier.layoutId()而不是第一个或第二个来选择您需要用作衡量其他人的参考的可组合项

在此输入图像描述