Horizo​​ntalPager 与 LazyColumn 位于另一个 LazyColumn 中 - Jetpack Compose

The*_*awd 16 android kotlin android-jetpack-compose lazycolumn jetpack-compose-accompanist

我想要与 TikTok 的个人资料屏幕类似的效果。顶部是ProfilPictureusername,下面是一个stickyHeader带有TabRow( Posts, Drafts, Likes, Favorites) 的 ,下面是HorizontalPager带有 4 个屏幕 ( Posts, Drafts, Likes, Favorites) 的 ,每个屏幕都包含一个列表。

如果我在 Compose 中构建它,我会崩溃,因为我无法将两个嵌套LazyColumns在一起。

这是我尝试做的事情的简短版本:

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()
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

PostList()例如,可组合项内部是:

@Composable
fun PostList(){
    LazyColumn() {
        items(50){ index ->
            Button(onClick = { /*TODO*/ },
                modifier = Modifier.fillMaxWidth()) {
                Text(text = "Button $index")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我得到的崩溃:

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.
Run Code Online (Sandbox Code Playgroud)

编辑: 给孩子LazyColumn一个固定的高度可以防止应用程序崩溃,但这并不是一个非常令人满意的解决方案。当 4 个列表的HorizontalPager大小不同时,它会产生奇怪且有缺陷的行为,而且看起来不正确。我尝试的另一件事是使用FlowRow而不是LazyColumn,这似乎也有效并修复了崩溃,但在这里我也得到了一个奇怪的行为,列表中HorizontalPager同时同步滚动,这不是我想要的。

HorizontalPager就是使这项任务如此困难的原因,没有它根本不是问题。

这是测试项目:https://github.com/DaFaack/TikTokScrollBehaviourCompose

LazyColumn这就是当我给出固定高度时的样子2500.dp,只有这么大的高度才能提供所需的滚动行为。这里的缺点是,即使列表为空,它的高度也为 2500,这会导致糟糕的用户体验,因为即使列表为空,它也允许用户滚动

在此输入图像描述

H.D*_*.D. 21

在这种情况下,使用可滚动Column而不是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")
            }
        }

    }
}
Run Code Online (Sandbox Code Playgroud)


And*_*i R 5

使用Material3对该行为进行简单的模拟

  1. 转到build.gradle并导入伴奏寻呼机
    因为 Compose 1.4 Pager 是 Compose 基础的一部分
  2. 使用PagerStateCoroutineScopeScrollBehaviorTabRow和创建 PagerScreen HorizontalPager,您可以使用不同scrolBehaviors提供的TopAppBarDefaults或实现您的自定义ScrollBehavior
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun PagerScreen() {
    // Tabs for pager
    val tabData = listOf(
        "Tab 1",
        "Tab 2",
    )

    // Pager state
    val pagerState = rememberPagerState()

    // Coroutine scope for scroll pager
    val coroutineScope = rememberCoroutineScope()

    // Scroll behavior for TopAppBar
    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())

    Scaffold(
        topBar = {
            TopAppBar(
                scrollBehavior = scrollBehavior,
                title = {
                    Text(text = "Top app bar")
                },
                colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
                    scrolledContainerColor = MaterialTheme.colorScheme.surface
                ),
            )
        },
        content = { innerPadding ->
            Column(
                modifier = Modifier
                    .padding(innerPadding)
                    .fillMaxSize(),
                content = {
                    TabRow(
                        selectedTabIndex = pagerState.currentPage,
                        tabs = {
                            tabData.forEachIndexed { index, title ->
                                Tab(
                                    text = { Text(title) },
                                    selected = pagerState.currentPage == index,
                                    onClick = {
                                        coroutineScope.launch {
                                            pagerState.animateScrollToPage(index)
                                        }
                                    },
                                )
                            }
                        }
                    )

                    HorizontalPager(
                        modifier = Modifier.fillMaxSize(),
                        pageCount = tabData.size,
                        state = pagerState,
                    ) { tabId ->
                        when (tabId) {
                            0 -> Tab1(scrollBehavior = scrollBehavior)
                            1 -> Tab2(scrollBehavior = scrollBehavior)
                        }
                    }
                }
            )
        }
    )
}
Run Code Online (Sandbox Code Playgroud)
  1. 创建选项卡HorizontalPager
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Tab1(scrollBehavior: TopAppBarScrollBehavior) {
    // List items
    val listItems = listOf(
        "test 1 tab 1",
        "test 2 tab 1",
        "test 3 tab 1",
        "test 4 tab 1",
        "test 5 tab 1",
        "test 6 tab 1",
        "test 7 tab 1",
        "test 8 tab 1",
        "test 9 tab 1",
        "test 10 tab 1",
        "test 11 tab 1",
        "test 12 tab 1",
    )

    val listState = rememberLazyListState()

    LazyColumn(
        modifier = Modifier
            .fillMaxWidth()
            .nestedScroll(scrollBehavior.nestedScrollConnection),
        state = listState,
        contentPadding = PaddingValues(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        content = {
            items(items = listItems) { item ->
                Card(
                    modifier = Modifier
                        .height(80.dp)
                        .fillMaxWidth(),

                    content = { Text(text = item) }
                )
            }
        }
    )
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Tab2(scrollBehavior: TopAppBarScrollBehavior) {
    // List items
    val listItems = listOf(
        "test 1 tab 2",
        "test 2 tab 2",
        "test 3 tab 2",
        "test 4 tab 2",
        "test 5 tab 2",
        "test 6 tab 2",
        "test 7 tab 2",
        "test 8 tab 2",
        "test 9 tab 2",
        "test 10 tab 2",
        "test 11 tab 2",
        "test 12 tab 2",
    )

    val listState = rememberLazyListState()

    LazyColumn(
        modifier = Modifier
            .fillMaxWidth()
            .nestedScroll(scrollBehavior.nestedScrollConnection),
        state = listState,
        contentPadding = PaddingValues(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        content = {
            items(items = listItems) { item ->
                Card(
                    modifier = Modifier
                        .height(80.dp)
                        .fillMaxWidth(),

                    content = { Text(text = item) }
                )
            }
        }
    )
}
Run Code Online (Sandbox Code Playgroud)

完整代码:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.ExperimentalFoundationApi
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.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import com.andreirozov.pager.ui.theme.PagerTheme
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PagerTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    PagerScreen()
                }

            }
        }
    }
}

@OptIn(ExperimentalPagerApi::class, ExperimentalMaterial3Api::class)
@Composable
fun PagerScreen() {
    // Tabs for pager
    val tabData = listOf(
        "Tab 1",
        "Tab 2",
    )

    // Pager state
    val pagerState = rememberPagerState()

    // Coroutine scope for scroll pager
    val coroutineScope = rememberCoroutineScope()

    // Scroll behavior for TopAppBar
    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())

    Scaffold(
        topBar = {
            TopAppBar(
                scrollBehavior = scrollBehavior,
                title = {
                    Text(text = "Top app bar")
                },
                colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
                    scrolledContainerColor = MaterialTheme.colorScheme.surface
                ),
            )
        },
        content = { innerPadding ->
            Column(
                modifier = Modifier
                    .padding(innerPadding)
                    .fillMaxSize(),
                content = {
                    TabRow(
                        selectedTabIndex = pagerState.currentPage,
                        tabs = {
                            tabData.forEachIndexed { index, title ->
                                Tab(
                                    text = { Text(title) },
                                    selected = pagerState.currentPage == index,
                                    onClick = {
                                        coroutineScope.launch {
                                            pagerState.animateScrollToPage(index)
                                        }
                                    },
                                )
                            }
                        }
                    )

                    HorizontalPager(
                        modifier = Modifier.fillMaxSize(),
                        count = tabData.size,
                        state = pagerState,
                    ) { tabId ->
                        when (tabId) {
                            0 -> Tab1(scrollBehavior = scrollBehavior)
                            1 -> Tab2(scrollBehavior = scrollBehavior)
                        }
                    }
                }
            )
        }
    )
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Tab1(scrollBehavior: TopAppBarScrollBehavior) {
    // List items
    val listItems = listOf(
        "test 1 tab 1",
        "test 2 tab 1",
        "test 3 tab 1",
        "test 4 tab 1",
        "test 5 tab 1",
        "test 6 tab 1",
        "test 7 tab 1",
        "test 8 tab 1",
        "test 9 tab 1",
        "test 10 tab 1",
        "test 11 tab 1",
        "test 12 tab 1",
    )

    val listState = rememberLazyListState()

    LazyColumn(
        modifier = Modifier
            .fillMaxWidth()
            .nestedScroll(scrollBehavior.nestedScrollConnection),
        state = listState,
        contentPadding = PaddingValues(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        content = {
            items(items = listItems) { item ->
                Card(
                    modifier = Modifier
                        .height(80.dp)
                        .fillMaxWidth(),

                    content = { Text(text = item) }
                )
            }
        }
    )
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Tab2(scrollBehavior: TopAppBarScrollBehavior) {
    // List items
    val listItems = listOf(
        "test 1 tab 2",
        "test 2 tab 2",
        "test 3 tab 2",
        "test 4 tab 2",
        "test 5 tab 2",
        "test 6 tab 2",
        "test 7 tab 2",
        "test 8 tab 2",
        "test 9 tab 2",
        "test 10 tab 2",
        "test 11 tab 2",
        "test 12 tab 2",
    )

    val listState = rememberLazyListState()

    LazyColumn(
        modifier = Modifier
            .fillMaxWidth()
            .nestedScroll(scrollBehavior.nestedScrollConnection),
        state = listState,
        contentPadding = PaddingValues(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        content = {
            items(items = listItems) { item ->
                Card(
                    modifier = Modifier
                        .height(80.dp)
                        .fillMaxWidth(),

                    content = { Text(text = item) }
                )
            }
        }
    )
}
Run Code Online (Sandbox Code Playgroud)

结果:

在此输入图像描述