Исправлена ​​сетка внутри LazyColumn в Jetpack Compose?

В настоящее время в Jetpack Compose этот код выдает IllegalStateException потому что вы не можете вложить два вертикально прокручиваемых Composable:

      @ExperimentalFoundationApi
@Composable
fun MyLazyColumn() {
    LazyColumn {
        item {
            Text(text = "My LazyColumn Title")
        }
        item {
            LazyVerticalGrid(cells = GridCells.Fixed(4)) {
                items(10) {
                    Box(
                        modifier = Modifier
                            .size(50.dp)
                            .padding(5.dp)
                            .background(Color.Gray)
                    )
                }
            }
        }
    }
}

Я не хочу, чтобы сама сетка прокручивалась, а просто отображала фиксированную сетку Composable, которую я в нее передаю. Есть ли обходной путь для отображения сетки без прокрутки внутри LazyColumn?

5 ответов

Решение

LazyVerticalGrid с фиксированным количеством ячеек имеет довольно простую внутреннюю компоновку, основанную на плюсе Row.

Вместо этого вы можете реализовать gridItems для использования с LazyColumn:

      fun LazyListScope.gridItems(
    count: Int,
    nColumns: Int,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    itemContent: @Composable BoxScope.(Int) -> Unit,
) {
    gridItems(
        data = List(count) { it },
        nColumns = nColumns,
        horizontalArrangement = horizontalArrangement,
        itemContent = itemContent,
    )
}

fun <T> LazyListScope.gridItems(
    data: List<T>,
    nColumns: Int,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    itemContent: @Composable BoxScope.(T) -> Unit,
) {
    val rows = if (data.count() == 0) 0 else 1 + (data.count() - 1) / nColumns
    items(rows) { rowIndex ->
        Row(horizontalArrangement = horizontalArrangement) {
            for (columnIndex in 0 until nColumns) {
                val itemIndex = rowIndex * nColumns + columnIndex
                if (itemIndex < data.count()) {
                    Box(
                        modifier = Modifier.weight(1f, fill = true),
                        propagateMinConstraints = true
                    ) {
                        itemContent.invoke(this, data[itemIndex])
                    }
                } else {
                    Spacer(Modifier.weight(1f, fill = true))
                }
            }
        }
    }
}

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

      LazyColumn {
    item {
        Text(text = "My LazyColumn Title")
    }
    // with count
    gridItems(10, nColumns = 4) { index -> 
        Box(
            modifier = Modifier
                .size(50.dp)
                .padding(5.dp)
                .background(Color.Gray)
        )
    }
    // or with list of items
    gridItems(listOf(1,2,3), nColumns = 4) { item ->
        Box(
            modifier = Modifier
                .size(50.dp)
                .padding(5.dp)
                .background(Color.Gray)
        )
    }
}

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

В моем случае мне удалось решить эту проблему, сделав LazyVerticalGrid в качестве контейнера для элементов сетки, а также другого содержимого, теперь это выглядит так:

      val spanCount = 2
LazyVerticalGrid(
    modifier = modifier
        .fillMaxWidth(),
    cells = GridCells.Fixed(spanCount),
    state = rememberLazyGridState(),
) {
    item(
        span = {
            /** Take a full row */
            GridItemSpan(currentLineSpan = spanCount)
        }
    ) {
        Column(
            modifier = Modifier.fillMaxWidth()
        ) {
            items.forEach {
                /** Iterate over your items here */
            }
        }
    }
    items(
        items = gridItems
    ){gridItem->
        /** Grid items go here */
    }
}

Для меня я добавил явное указание высоты, и оно отлично работало со мной для LazyverticalGrid внутри ленивого столбца.

       LazyVerticalGrid(
        cells = GridCells.Fixed(3),
        contentPadding = PaddingValues(5.dp),
        modifier = Modifier
            .layoutId("lazy_list")
            .height(200.dp)

В настоящее время в Jetpack Compose этот код выдает исключение IllegalStateException, поскольку невозможно вложить два компонента Composable с вертикальной прокруткой:

Это не совсем так: здесь говорится, что вы не можете передать Constraints.Infinity в другой Composable с возможностью прокрутки. Составные элементы с прокруткой, поскольку LazyLists также полагаются на модификатор прокрутки, не могут измерять себя с помощью maxWidth/Height, равного бесконечности. Если вы измените maxHeight на что-то конечное, у вас не возникнет проблемы. Ограничения означают диапазон измерения компонуемых объектов перед их размещением. Если вы передадите 0-конечное измерение, оно будет измерено в измерении.

У вас также нет фиксированной высоты, если ваш контент в вертикальной сетке меньше родительской высоты - суммы высоты другого контента.

Передавая его с помощью Modifier.heighIn(max), вы ограничиваете измерение высоты сетки до 0 и между родительскими элементами.

      @Preview
@Composable
private fun LazyTest() {
    BoxWithConstraints {
        val parentHeight = maxHeight
        LazyColumn {
            item {
                Text(text = "My LazyColumn Title")
            }
            item {
                LazyVerticalGrid(
                    modifier = Modifier.heightIn(max = parentHeight),
                    columns = GridCells.Fixed(4)
                ) {
                    items(10) {
                        Box(
                            modifier = Modifier
                                .size(50.dp)
                                .padding(5.dp)
                                .background(Color.Gray)
                        )
                    }
                }
            }
        }
    }
}

Если в вашем случае вы хотите, чтобы Grid покрыл остальную часть высоты LazyColumn, вы можете измерить высоту других элементов и использовать Modifier.heigh(родительская высота - сумма высот другого контента), на самом деле это довольно просто с помощью специальногоLayout.

И если вам интересно, почему возникает это исключение, оно находится в коде прокрутки, потому что они выполняют проверку перед установкой компоновки с модификатором прокрутки as.

      private class ScrollingLayoutNode(
    var scrollerState: ScrollState,
    var isReversed: Boolean,
    var isVertical: Boolean
) : LayoutModifierNode, Modifier.Node() {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // And here in LazyVerticalGrid there is a check if there is Constraints.Infinity
        checkScrollableContainerConstraints(
            constraints,
            if (isVertical) Orientation.Vertical else Orientation.Horizontal
        )

        //  this is how LazyColumn passes Constraints.Infinity content or child Composables
        val childConstraints = constraints.copy(
            maxHeight = if (isVertical) Constraints.Infinity else constraints.maxHeight,
            maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity
        )
        val placeable = measurable.measure(childConstraints)
        val width = placeable.width.coerceAtMost(constraints.maxWidth)
        val height = placeable.height.coerceAtMost(constraints.maxHeight)
        val scrollHeight = placeable.height - height
        val scrollWidth = placeable.width - width
        val side = if (isVertical) scrollHeight else scrollWidth
        // The max value must be updated before returning from the measure block so that any other
        // chained RemeasurementModifiers that try to perform scrolling based on the new
        // measurements inside onRemeasured are able to scroll to the new max based on the newly-
        // measured size.

}

ИcheckScrollableContainerConstraintsфункция

      fun checkScrollableContainerConstraints(
    constraints: Constraints,
    orientation: Orientation
) {
    if (orientation == Orientation.Vertical) {
        check(constraints.maxHeight != Constraints.Infinity) {
            "Vertically scrollable component was measured with an infinity maximum height " +
                "constraints, which is disallowed. One of the common reasons is nesting layouts " +
                "like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a " +
                "header before the list of items please add a header as a separate item() before " +
                "the main items() inside the LazyColumn scope. There are could be other reasons " +
                "for this to happen: your ComposeView was added into a LinearLayout with some " +
                "weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a " +
                "custom layout. Please try to remove the source of infinite constraints in the " +
                "hierarchy above the scrolling container."
        }
    } else {
        check(constraints.maxWidth != Constraints.Infinity) {
            "Horizontally scrollable component was measured with an infinity maximum width " +
                "constraints, which is disallowed. One of the common reasons is nesting layouts " +
                "like LazyRow and Row(Modifier.horizontalScroll()). If you want to add a " +
                "header before the list of items please add a header as a separate item() before " +
                "the main items() inside the LazyRow scope. There are could be other reasons " +
                "for this to happen: your ComposeView was added into a LinearLayout with some " +
                "weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a " +
                "custom layout. Please try to remove the source of infinite constraints in the " +
                "hierarchy above the scrolling container."
        }
    }
}

Если ваша сетка маленькая и не слишком сложная (например, требует отложенной загрузки), вы можете использовать Jetpack Compose ConstraintLayout . Я столкнулся с этим, когда у меня был прокручиваемый родительский элемент, который я не мог изменить.

Он работает почти так же, как ConstraintLayout на основе представления, хотя и немного более ограниченно.

Например, это будет сетка точек маркера, которая выравнивает текстовые строки по самой длинной ширине маркера:

      val spaceAfterBullet = 8.dp
val spaceBetweenRows = 4.dp

ConstraintLayout(modifier = Modifier.fillMaxWidth()) {

    val (bullet1, bullet2, bullet3, text1, text2, text3) = createRefs()
    val bulletBarrier = createEndBarrier(bullet1, bullet2, bullet3)
    val row1Barrier = createBottomBarrier(bullet1, text1)
    val row2Barrier = createBottomBarrier(bullet2, text2)

    Text(text = "1.", textAlign = TextAlign.End, modifier = Modifier.constrainAs(bullet1) {
    })
    Text(text = "First line of text", modifier = Modifier
        .padding(start = spaceAfterBullet)
        .constrainAs(text1) {
            start.linkTo(bulletBarrier)
        })

    Text(text = "2.", textAlign = TextAlign.End, modifier = Modifier.constrainAs(bullet2) {
        top.linkTo(row1Barrier, margin = spaceBetweenRows)
    })
    Text(
        text = "Second line of text which will overflow into two lines of text :)",
        modifier = Modifier
            .padding(start = spaceAfterBullet)
            .constrainAs(text2) {
                start.linkTo(bulletBarrier)
                top.linkTo(row1Barrier, margin = spaceBetweenRows)
            })

    Text(text = "99.", textAlign = TextAlign.End, modifier = Modifier.constrainAs(bullet3) {
        top.linkTo(row2Barrier, margin = spaceBetweenRows)
    })
    Text(
        text = "\"Every man has two lives, and the second starts when he realizes he has just one\" — Confucius",
        modifier = Modifier
            .padding(start = spaceAfterBullet)
            .constrainAs(text3) {
                start.linkTo(bulletBarrier)
                top.linkTo(row2Barrier, margin = spaceBetweenRows)
            })

}

Я также создал здесь общий вариант , который допускает повторное использование.

Другие вопросы по тегам