Заголовки и подзаголовки LazyColum
Я пытаюсь создать
LazyColumn
с липким заголовком и липким подзаголовком. Функциональность, которую я хотел бы достичь, демонстрируется в этом GIF-изображении.
Мой исходный код выглядит следующим образом:
Элемент данных, который будет использоваться
class Item(
val type: String, // Will be used as header
val subType: String, // Will be used as sub-header
val title: String,
val description: String,
val imageUrl: String)
Пример использования предмета
Item("Movie", "Action",
"Shang-Chi and the Legend of the Ten Rings",
"Shang-Chi, the master of unarmed weaponry-based Kung Fu, is forced to " +
"confront his past after being drawn into the Ten Rings organization.",
"https://threepixelslab.gr/wp-content/uploads/2021/04/Shang-Chi-and-the-Legend-of-the-Ten-Rings.jpg")
Составление списка
@Composable
fun MainList(data: List<Item>) {
val mainGroup = data.groupBy { it.type }
LazyColumn {
mainGroup.forEach { (type, groupedData) ->
val subGroup = groupedData.groupBy { it.subType }
stickyHeader {
Header(text = type)
}
item {
LazyColumn {
subGroup.forEach { (subType, subGroupedData) ->
stickyHeader { Header(text = subType) }
items(subGroupedData) {
SimpleItem(item = it)
}
}
}
}
}
}
}
Ошибка
java.lang.IllegalStateException: вложение прокручиваемых в одном направлении макетов, таких как LazyColumn и Column(Modifier.verticalScroll()), не допускается. Если вы хотите добавить заголовок перед списком элементов, обратите внимание на компонент LazyColumn, который имеет API-интерфейс DSL, который позволяет сначала добавить заголовок с помощью функции item (), а затем список элементов с помощью items ().
на androidx.compose.foundation.ScrollKt.assertNotNestingScrollableContainers-K40F9xA(Scroll.kt:370) на androidx.compose.foundation.lazy.LazyListKt$LazyList$1.invoke-0kLqBqw(LazyList.kt:96.found.com) на androidx.compose.kt: 96.found.com lazy.LazyListKt$LazyList$1.invoke(LazyList.kt:95) в androidx.compose.ui.layout.SubcomposeLayoutState$createMeasurePolicy$1.measure-3p2s80s(SubcomposeLayout.kt:345) на androidx.compose.ui.noable. measure-BRTryo0(InnerPlaceable.kt:43) на androidx.compose.foundation.layout.PaddingValuesModifier.measure-3p2s80s(Padding.kt:417) на androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedktLayoutNode.39) на androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s(GraphicsLayerModifier.kt:219) на androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedLayoutNode.kt: 39) на androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116) на androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116) на androidx.commeasure-BRTryo0(DelegatingLayoutNodeWrapper.kt:116) на androidx.compose.ui.node.OuterMeasurablePlaceable$ Remeasure $3.invoke(OuterMeasurablePlaceable.kt:100) на androidx.compose.ui.node.OuterMeasurablePlaceable$ Remeasure $3.invokekt:99) по адресу androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:128) по адресу androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:75).node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:63) на androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:99) на androidx.compose.ui.node.OuterMeasurablePlaceable.measure-BRTryo0(OuterMeasurablePlaceable.kt:71) на androidx.compose.out. measure-BRTryo0(LayoutNode.kt:1227) по адресу androidx.compose.foundation.layout.RowColumnImplKt$rowColumnMeasurePolicy$1.measure-3p2s80s(RowColumnImpl.kt:89) по адресу androidxlace.compose.ui.node.InnerPlaceable.measure-BR .kt: 43) на androidx.compose.ui.node.OuterMeasurablePlaceable$ Remeasure $3.invoke(OuterMeasurablePlaceable.kt:100) на androidx.compose.ui.node.OuterMeasurablePlaceable$ Remeasure $ 3.invoke (OuterMeasurablePlaceable.kt: 99) at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:128) на androidx.compose.ui.node.OwnerSnapshotObserver.Наблюдение за $ ui_release (OwnerSnapshotObserver.kt: 75) на androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads $ ui_release (OwnerSnapshotObserver.kt: 63) на androidx.compose.ui.noderemoderemoderemoderemoderemoderemoderemoderemoderemoderemoderemoderemodereme.OuterMeaseable 99) на androidx.compose.ui.node.OuterMeasurablePlaceable.measure-BRTryo0(OuterMeasurablePlaceable.kt:71) на androidx.compose.ui.node.LayoutNode.measure-BRTryo0(LayoutNode.kt:1227) на androidx.compose.foundation.lazy.LazyMeasuredItemProvider.getAndMeasure-ZjPyQlc(LazyMeasuredItemProvider.kt:50) по адресу androidx.compose.foundation.lazy.LazyListMeasureKt.measureLazyList-9CW8viI($ 1. invoke-0kLqBqw(LazyList.kt:152) на androidx.compose.foundation.lazy.LazyListKt$LazyList$1.invoke(LazyList.kt:95) на androidx.compose.ui.layout.SubcomposeLayoutState$createMeasurePolicy$1.measure-3p2s80s(SubcomposeLayout.kt:345)
Можно ли реализовать такой список в Compose?
1 ответ
Решение, которое я нашел, - добавить элемент заголовка выше
LazyColumn
и динамически контролировать содержимое и видимость внутренних заголовков.
Ознакомьтесь с комментариями в приведенном ниже коде. Вот код:
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberImagePainter
import coil.transform.CircleCropTransformation
import learning.android.miltiheaderlist.ui.theme.MiltiHeaderListTheme
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.N)
@ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MiltiHeaderListTheme {
Surface(color = MaterialTheme.colors.background) {
MainList(getTheData())
}
}
}
}
}
@RequiresApi(Build.VERSION_CODES.N)
@ExperimentalFoundationApi
@Composable
fun MainList(data: List<Item>) {
val listState = rememberLazyListState()
val header = remember {
mutableStateOf(data[0].type)
} // Use state to achieve re-compose
val mainGroup = data.groupBy { it.type }
Column() {
Header(header = header.value) // This header's content will be updated while scrolling
LazyColumn(state = listState) {
mainGroup.forEach { (type, groupedData) ->
if (header.value !== type) {
stickyHeader { Header(header = type) }// This header visibility will be updated while scrolling
}
val subGroup = groupedData.groupBy { it.subType }
subGroup.forEach { (subtype, subGroupedData)->
stickyHeader {
Header(subheader = "$subtype (${listState.firstVisibleItemIndex})") // Create only the subheader item
}
items(items = subGroupedData) {
//listState.firstVisibleItemIndex.toString()
SimpleItem(item = it)
if (listState.firstVisibleItemIndex-2 >= 0) {
header.value = data[listState.firstVisibleItemIndex-2].type // This is doing the magic for visibility and content of the above headers
}
}
}
}
}
}
}
@Composable
fun SimpleItem(item : Item) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(5.dp)
.clickable { },
elevation = 10.dp
) {
Row() {
Image(
painter = rememberImagePainter(
data = item.imageUrl,
builder = {
transformations(CircleCropTransformation())
}
),
contentDescription = null,
modifier = Modifier
.size(80.dp)
.padding(5.dp)
)
Column() {
Text(text = item.title, fontSize = 15.sp)
Text(text = item.description, fontSize = 10.sp)
}
}
}
}
@Composable
fun Header(header: String = "", subheader: String = "") {
if (header.isNotEmpty()) {
Card(
modifier = Modifier
.fillMaxWidth(),
elevation = 5.dp,
backgroundColor = Color.Red
) {
Text(text = header, fontSize = 20.sp, modifier = Modifier.padding(5.dp))
}
}
if (subheader.isNotEmpty()) {
Card(
modifier = Modifier
.fillMaxWidth(),
elevation = 5.dp,
backgroundColor = Color.Red
) {
Text(text = subheader, fontSize = 20.sp, modifier = Modifier.padding(5.dp))
}
}
}
Другая часть отсутствует - это данные, которые показаны ниже:
class Item(
val type: String = "",
val subType: String = "",
val title: String = "",
val description: String = "",
val imageUrl: String = "",
)
fun getTheData(): List<Item> {
return listOf(
Item("Movie", "Action",
"Shang-Chi and the Legend of the Ten Rings",
"Shang-Chi, the master of unarmed weaponry-based Kung Fu, is forced to " +
"confront his past after being drawn into the Ten Rings organization.",
"https://threepixelslab.gr/wp-content/uploads/2021/04/Shang-Chi-and-the-Legend-of-the-Ten-Rings.jpg"),
Item("Movie", "Action",
"The Matrix Resurrections",
"The plot is currently unknown.",
"https://media.oneman.gr/onm-images/matrix-3.jpg"),
Item("Movie", "Action",
"Free Guy",
"A bank teller discovers that he's actually an NPC inside a brutal, open world video game.",
"https://www.athinorama.gr/lmnts/events/cinema/10072050/Poster.jpg"),
Item("Movie", "Action",
"The Suicide Squad",
"Supervillains Harley Quinn, Bloodsport, Peacemaker and a collection of nutty " +
"cons at Belle Reve prison join the super-secret, super-shady Task Force X as they " +
"are dropped off at the remote, enemy-infused island of Corto Maltese.",
"https://sm.ign.com/t/ign_gr/movie/s/suicide-sq/suicide-squad-2_9h82.200.jpg"),
Item("Movie", "Action",
"Kate",
"A female assassin has 24 hours to get vengeance on her murderer before she dies.",
"https://image.tmdb.org/t/p/w185/uQWgSRXeYRWCvGIX9LDNBW6XBYD.jpg"),
Item("Movie", "Horror",
"Candyman",
"A sequel to the horror film Candyman (1992) that returns to the now-gentrified " +
"Chicago neighborhood where the legend began.",
"https://upreviews.net/images/artwork/upreviews_-KiisqOOM-GwNoLaCO_V.jpg"),
Item("Movie", "Horror",
"Don't Breathe 2",
"The sequel is set in the years following the initial deadly home invasion, " +
"where Norman Nordstrom (Stephen Lang) lives in quiet solace until his past sins " +
"catch up to him.",
"https://images-na.ssl-images-amazon.com/images/I/914Wg3bzCGL._RI_.jpg"),
Item("Movie", "Horror",
"Last Night in Soho",
"An aspiring fashion designer is mysteriously able to enter the 1960s where " +
"she encounters a dazzling wannabe singer. But the glamour is not all it appears " +
"to be and the dreams of the past start to crack and splinter into something darker.",
"https://deadline.com/wp-content/uploads/2021/06/last-night-in-soho-crop-excl-2.jpg"),
Item("Movie", "Horror",
"Malignant",
"Madison is paralyzed by shocking visions of grisly murders, and her torment " +
"worsens as she discovers that these waking dreams are in fact terrifying realities.",
"https://media.oneman.gr/onm-images/HMKQGC5Q2NGSBCA3TIGZARMZFU.jpg"),
Item("Book", "Horror",
"My Heart Is a Chainsaw",
"In her quickly gentrifying rural lake town Jade sees recent events only " +
"her encyclopedic knowledge of horror films could have prepared her for",
"https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1623264202l/55711617.jpg"),
Item("Book", "Horror",
"The Dead and the Dark",
"Courtney Gould’s thrilling debut The Dead and the Dark is about the things " +
"that lurk in dark corners, the parts of you that can’t remain hidden, and about " +
"finding home in places―and people―you didn’t expect.",
"https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1599058814l/53141419.jpg"),
Item("Book", "Horror",
"Billy Summers",
"Billy Summers is a man in a room with a gun. He’s a killer for hire and the " +
"best in the business.",
"https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1618151020l/56852407.jpg"),
Item("Book", "Horror",
"A Lesson in Vengeance",
"Perched in the Catskill mountains, the centuries-old, ivy-covered campus was " +
"home until the tragic death of her girlfriend.",
"https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1605799295l/50999821.jpg"),
Item("Book", "Horror",
"Velvet Was the Night",
"From the New York Times bestselling author of Mexican Gothic comes a " +
"“delicious, twisted treat for lovers of noir” about a daydreaming secretary, a " +
"lonesome enforcer, and the mystery of a missing woman they’re both desperate to find.",
"https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1617426360l/54746205.jpg"),
Item("Book", "Comic",
"Star Wars: War Of The Bounty Hunters - Boushh",
"THE SECRET ORIGIN OF BOUSHH! A “WAR OF THE BOUNTY HUNTERS” TIE-IN!",
"https://i.annihil.us/u/prod/marvel/i/mg/f/90/6142606a6160a/clean.jpg"),
Item("Book", "Comic",
"X-Men: The Trial of Magneto",
"Heroes of the Marvel Universe came to Krakoa for a memorial.",
"https://i.annihil.us/u/prod/marvel/i/mg/7/03/61426068ad880/clean.jpg"),
Item("Book", "Comic",
"Extreme Carnage: Agony",
"As the odds (and symbiotes!) stack against our heroes, is there any way " +
"they can win against Carnage?",
"https://i.annihil.us/u/prod/marvel/i/mg/3/80/614260a6d95c8/clean.jpg"),
Item("Book", "Comic",
"The Last Annihilation: Wakanda",
"With the universe itself at stake, Black Panther enlists the might of the " +
"Intergalactic Empire of Wakanda to help stop the dreaded Dormammu!",
"https://i.annihil.us/u/prod/marvel/i/mg/3/90/61426088ebb66/clean.jpg"),
Item("Book", "Comic",
"Black Widow",
"FRIEND OR FOE?",
"https://i.annihil.us/u/prod/marvel/i/mg/6/90/612e8e0826ce5/clean.jpg")
)
}