带有 LazyColum 的 HorizontalPager 在另一个 LazyColum 中 - Jetpack Compose
HorizontalPager with LazyColum inside another LazyColum - Jetpack Compose
我想要与 TikToks 个人资料屏幕类似的效果。
上面是 ProfilPicture
和 username
,下面是 stickyHeader
和 TabRow
(Posts
, Drafts
, Likes
, Favorites
) 下面是一个 HorizontalPager
和 4 个屏幕 (Posts
, Drafts
, Likes
, Favorites
), 每个屏幕包含一个列表。
如果我在 Compose 中构建它,我会崩溃,因为我无法将两个 LazyColums
嵌套在一起。
这是我尝试做的简短版本:
val tabList = listOf("Posts", "Drafts", "Likes", "Favorites")
val pagerState: PagerState = rememberPagerState(initialPage = 0)
val coroutineScope = rememberCoroutineScope()
LazyColumn(modifier = Modifier.fillMaxSize()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
contentAlignment = Alignment.Center
) {
//Profile Header (Picture, Username, Followers, etc)
Text(text = "Profile Picture")
}
}
stickyHeader {
TabRow(
modifier = Modifier.fillMaxWidth(),
backgroundColor = Color.Black,
contentColor = Color.White,
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions)
)
}
) {
// Add tabs for all of our pages
tabList.forEachIndexed { index, title ->
Tab(
text = { Text(title) },
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
)
}
}
}
item {
HorizontalPager(
state = pagerState,
count = tabList.size
) { page: Int ->
when (page) {
0 -> PostsList()
1 -> DraftsList()
2 -> LikesList()
else -> FavoritesList()
}
}
}
}
并且在 PostList()
可组合项内例如是:
@Composable
fun PostList(){
LazyColumn() {
items(50){ index ->
Button(onClick = { /*TODO*/ },
modifier = Modifier.fillMaxWidth()) {
Text(text = "Button $index")
}
}
}
}
这是我遇到的崩溃:
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.
编辑:
给 child LazyColumn
一个固定的高度可以防止应用程序崩溃,但这不是一个非常令人满意的解决方案。
当 HorizontalPager
中的 4 个列表有不同的大小时,它会产生奇怪的错误行为,而且看起来不正确。
我尝试的另一件事是使用 FlowRow
而不是 LazyColumn
,这似乎也有效并修复了崩溃,但在这里我得到了一个奇怪的行为,HorizontalPager
中的列表正在同步滚动同时,这不是我想要的。
HorizontalPager
是这个任务如此困难的原因,没有它根本不是问题。
这里是测试项目:https://github.com/DaFaack/TikTokScrollBehaviourCompose
这就是我给 LazyColumn
固定高度 2500.dp
时的样子,只有如此大的高度才能提供所需的滚动行为。
这里的缺点是,即使列表为空,它的高度为 2500,这会导致糟糕的用户体验,因为即使列表为空,它也允许用户滚动
在这种情况下,在外层使用可滚动的 Row
而不是 LazyColumn
更容易。
这应该能达到你想要的效果:
package com.fujigames.nestedscrolltest
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import com.fujigames.nestedscrolltest.ui.theme.NestedScrollTestTheme
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalPagerApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NestedScrollTestTheme {
BoxWithConstraints {
val screenHeight = maxHeight
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollState)
) {
Box(
modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.background(Color.LightGray), contentAlignment = Alignment.Center
) {
Text(text = "HEADER")
}
Column(modifier = Modifier.height(screenHeight)) {
val tabList = listOf("Tab1", "Tab2")
val pagerState = rememberPagerState(initialPage = 0)
val coroutineScope = rememberCoroutineScope()
TabRow(
modifier = Modifier.fillMaxWidth(),
backgroundColor = Color.White,
contentColor = Color.Black,
selectedTabIndex = pagerState.currentPage,
// Override the indicator, using the provided pagerTabIndicatorOffset modifier
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions)
)
}
) {
tabList.forEachIndexed { index, title ->
Tab(
text = { Text(title) },
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
)
}
}
HorizontalPager(
state = pagerState,
count = tabList.size,
modifier = Modifier
.fillMaxHeight()
.nestedScroll(remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
return if (available.y > 0) Offset.Zero else Offset(
x = 0f,
y = -scrollState.dispatchRawDelta(-available.y)
)
}
}
})
) { page: Int ->
when (page) {
0 -> ListLazyColumn(50)
1 -> ListFlowRow(5)
}
}
}
}
}
}
}
}
}
@Composable
fun ListLazyColumn(items: Int) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { index ->
Button(
onClick = { /*TODO*/ },
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Button $index")
}
}
}
}
@Composable
fun ListFlowRow(items: Int) {
FlowRow(modifier = Modifier.fillMaxSize()) {
repeat(items) { index ->
Button(
onClick = { /*TODO*/ },
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Button $index")
}
}
}
}
我想要与 TikToks 个人资料屏幕类似的效果。
上面是 ProfilPicture
和 username
,下面是 stickyHeader
和 TabRow
(Posts
, Drafts
, Likes
, Favorites
) 下面是一个 HorizontalPager
和 4 个屏幕 (Posts
, Drafts
, Likes
, Favorites
), 每个屏幕包含一个列表。
如果我在 Compose 中构建它,我会崩溃,因为我无法将两个 LazyColums
嵌套在一起。
这是我尝试做的简短版本:
val tabList = listOf("Posts", "Drafts", "Likes", "Favorites")
val pagerState: PagerState = rememberPagerState(initialPage = 0)
val coroutineScope = rememberCoroutineScope()
LazyColumn(modifier = Modifier.fillMaxSize()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
contentAlignment = Alignment.Center
) {
//Profile Header (Picture, Username, Followers, etc)
Text(text = "Profile Picture")
}
}
stickyHeader {
TabRow(
modifier = Modifier.fillMaxWidth(),
backgroundColor = Color.Black,
contentColor = Color.White,
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions)
)
}
) {
// Add tabs for all of our pages
tabList.forEachIndexed { index, title ->
Tab(
text = { Text(title) },
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
)
}
}
}
item {
HorizontalPager(
state = pagerState,
count = tabList.size
) { page: Int ->
when (page) {
0 -> PostsList()
1 -> DraftsList()
2 -> LikesList()
else -> FavoritesList()
}
}
}
}
并且在 PostList()
可组合项内例如是:
@Composable
fun PostList(){
LazyColumn() {
items(50){ index ->
Button(onClick = { /*TODO*/ },
modifier = Modifier.fillMaxWidth()) {
Text(text = "Button $index")
}
}
}
}
这是我遇到的崩溃:
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.
编辑:
给 child LazyColumn
一个固定的高度可以防止应用程序崩溃,但这不是一个非常令人满意的解决方案。
当 HorizontalPager
中的 4 个列表有不同的大小时,它会产生奇怪的错误行为,而且看起来不正确。
我尝试的另一件事是使用 FlowRow
而不是 LazyColumn
,这似乎也有效并修复了崩溃,但在这里我得到了一个奇怪的行为,HorizontalPager
中的列表正在同步滚动同时,这不是我想要的。
HorizontalPager
是这个任务如此困难的原因,没有它根本不是问题。
这里是测试项目:https://github.com/DaFaack/TikTokScrollBehaviourCompose
这就是我给 LazyColumn
固定高度 2500.dp
时的样子,只有如此大的高度才能提供所需的滚动行为。
这里的缺点是,即使列表为空,它的高度为 2500,这会导致糟糕的用户体验,因为即使列表为空,它也允许用户滚动
在这种情况下,在外层使用可滚动的 Row
而不是 LazyColumn
更容易。
这应该能达到你想要的效果:
package com.fujigames.nestedscrolltest
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import com.fujigames.nestedscrolltest.ui.theme.NestedScrollTestTheme
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalPagerApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NestedScrollTestTheme {
BoxWithConstraints {
val screenHeight = maxHeight
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollState)
) {
Box(
modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.background(Color.LightGray), contentAlignment = Alignment.Center
) {
Text(text = "HEADER")
}
Column(modifier = Modifier.height(screenHeight)) {
val tabList = listOf("Tab1", "Tab2")
val pagerState = rememberPagerState(initialPage = 0)
val coroutineScope = rememberCoroutineScope()
TabRow(
modifier = Modifier.fillMaxWidth(),
backgroundColor = Color.White,
contentColor = Color.Black,
selectedTabIndex = pagerState.currentPage,
// Override the indicator, using the provided pagerTabIndicatorOffset modifier
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions)
)
}
) {
tabList.forEachIndexed { index, title ->
Tab(
text = { Text(title) },
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
)
}
}
HorizontalPager(
state = pagerState,
count = tabList.size,
modifier = Modifier
.fillMaxHeight()
.nestedScroll(remember {
object : NestedScrollConnection {
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
return if (available.y > 0) Offset.Zero else Offset(
x = 0f,
y = -scrollState.dispatchRawDelta(-available.y)
)
}
}
})
) { page: Int ->
when (page) {
0 -> ListLazyColumn(50)
1 -> ListFlowRow(5)
}
}
}
}
}
}
}
}
}
@Composable
fun ListLazyColumn(items: Int) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { index ->
Button(
onClick = { /*TODO*/ },
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Button $index")
}
}
}
}
@Composable
fun ListFlowRow(items: Int) {
FlowRow(modifier = Modifier.fillMaxSize()) {
repeat(items) { index ->
Button(
onClick = { /*TODO*/ },
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Button $index")
}
}
}
}