簡體   English   中英

Jetpack Compose 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 }

它應該根據另一個組件大小重新測量一個組件,但這段代碼實際上做了什么對我來說是個謎。

  1. subcompose組合 function 如何工作?
  2. slotId有什么意義,我們可以通過某種方式獲得 slotId 嗎?

subCompose 的描述subCompose

使用給定的 slotId 對提供的內容執行子組合。 參數:slotId - 唯一的 id,代表我們正在組合的插槽。 如果您有固定數量或插槽,您可以使用枚舉作為插槽 ID,或者如果您有一個項目列表,列表中的索引或其他一些唯一鍵可以工作。 為了能夠正確匹配重新測量之間的內容,您應該提供 object,它等於您在上一次測量期間使用的那個。 content - 定義插槽的可組合內容。 它可以發出多個布局,在這種情況下,返回的 Measurables 列表將包含多個元素。

有人可以解釋它的含義或/並為SubcomposeLayout提供工作示例嗎?

它應該根據另一個組件尺寸重新測量一個組件......

SubcomposeLayout不會重新測量。 它允許推遲內容的組成和測量,直到知道其來自其父項的約束並且可以測量其某些內容,其結果可以作為參數傳遞給推遲的內容。 上面的示例計算mainContent生成的內容的最大大小,並將其作為參數傳遞給deferredContent 然后它測量deferredContent並將mainContentdeferredContent放在彼此的頂部。

如何使用的最簡單的例子SubcomposeLayoutBoxWithConstraints就是只傳遞它從它的父直接接收到其內容的約束。 在布局期間發生的父級測量了框的兄弟姐妹之前,框的約束是未知的,因此content的組合被推遲到布局。

同樣,對於上面的示例, mainContentmaxSize直到布局才知道,因此一旦計算出maxSize就會在布局中調用deferredContent 它總是將deferredContent放在mainContent之上,因此假定deferredContent以某種方式使用maxSize以避免模糊mainContent生成的內容。 可能不是可組合的最佳設計,但可組合的目的是說明性而不是本身有用。

請注意,可以在layout塊中多次調用subcompose 例如,這就是LazyRow發生的情況。 slotId允許SubcomposeLayout跟蹤和管理通過調用創建的成分subcompose 例如,如果你是從一個陣列產生的內容,你可能希望使用數組作為它的指數slotId允許SubcomposeLayout確定哪些subcompose產生應重構期間被用來最后一次。 此外,如果一個slotid不使用任何更多, SubcomposeLayout將配置其相應的組合物。

而對於其中slotId雲,即至的來電SubcomposeLayout 如果內容需要它,請將其作為參數傳遞。 上面的例子不需要它,因為對於deferredContentslotId總是相同的,所以它不需要去任何地方。

我根據官方文檔提供的示例和@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
}

結果

在此處輸入圖像描述

Github 代碼鏈接

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM