onebone / compose-collapsing-toolbar

A simple implementation of collapsing toolbar for Jetpack Compose
MIT License
530 stars 77 forks source link

CollapsingToolbarScaffoldState.toolbarState.progress changes without scrolling #27

Open aurimas-zarskis opened 2 years ago

aurimas-zarskis commented 2 years ago

CollapsingToolbarScaffoldState.toolbarState.progress value changes when you go back in navigation stack even though no scrolling is happening. If toolbar title size depends on progress value, it visibly changes from smallest to largest when navigating back to screen. It seems that when navigating back, two progress values are dispatched, 0.0f and then 1.0f.

onebone commented 2 years ago

Can you provide a minimal code snippet to help me reproduce your issue?

aurimas-zarskis commented 2 years ago

I am using AnimatedNavHost from accompanist with no animation when switching between destinations. Regular NavHost with fade animation actually hides text size change, but adding breakpoint to text size calculation you can see that progress is dispatched multiple times without scroll

    @OptIn(ExperimentalAnimationApi::class)
    @Composable
    private fun Test() {
        val navController = rememberNavController()
        Column(modifier = Modifier.fillMaxSize()) {
            NavHost(
                navController = navController,
                startDestination = "first",
                modifier = Modifier.fillMaxSize().weight(1f)
            ) {
                composable("first"
                ) { FirstScreen() }
                composable("second") { SecondScreen() }
            }

            BottomNav(navController = navController)
        }
    }

    @Composable
    private fun BottomNav(navController: NavHostController) {
        BottomNavigation() {
            val navBackStackEntry by navController.currentBackStackEntryAsState()
            val currentDestination = navBackStackEntry?.destination

            BottomNavigationItem(
                icon = { Icon(Icons.Default.Home, contentDescription = null) },
                label = { Text("First") },
                selected = currentDestination?.hierarchy?.any { it.route == "first" } == true,
                onClick = {
                    navController.navigate("first") {
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )

            BottomNavigationItem(
                icon = { Icon(Icons.Default.Home, contentDescription = null) },
                label = { Text("Second") },
                selected = currentDestination?.hierarchy?.any { it.route == "second" } == true,
                onClick = {
                    navController.navigate("second") {
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )
        }
    }

    @Composable
    private fun FirstScreen() {
        val state = rememberCollapsingToolbarScaffoldState()
        CollapsingToolbarScaffold(
            modifier = Modifier
                .fillMaxSize(),
            state = state,
            scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
            toolbar = {
                val textSize = remember(state.toolbarState.progress) {
                    (22 + (34 - 22) * state.toolbarState.progress).sp
                }

                Box(
                    modifier = Modifier
                        .background(Color.Gray)
                        .fillMaxWidth()
                        .height(120.dp)
                        .pin()
                )

                Text(
                    text = "First",
                    fontSize = textSize,
                    modifier = Modifier
                        .road(Alignment.CenterStart, Alignment.BottomStart)
                        .padding(
                            start = 16.dp,
                            end = 16.dp,
                            top = 18.dp,
                            bottom = 18.dp
                        ),
                )
            }
        ) {
            Column(
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier
                    .fillMaxSize()
                    .verticalScroll(rememberScrollState())
            ) {
                Text(text = "First screen")
            }
        }
    }

    @Composable
    private fun SecondScreen() {
        val state = rememberCollapsingToolbarScaffoldState()
        CollapsingToolbarScaffold(
            modifier = Modifier
                .fillMaxSize(),
            state = state,
            scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
            toolbar = {
                val textSize = remember(state.toolbarState.progress) {
                    (22 + (34 - 22) * state.toolbarState.progress).sp
                }

                Box(
                    modifier = Modifier
                        .background(Color.Gray)
                        .fillMaxWidth()
                        .height(120.dp)
                        .pin()
                )

                Text(
                    text = "Second",
                    fontSize = textSize,
                    modifier = Modifier
                        .road(Alignment.CenterStart, Alignment.BottomStart)
                        .padding(
                            start = 16.dp,
                            end = 16.dp,
                            top = 18.dp,
                            bottom = 18.dp
                        ),
                )
            }
        ) {
            Column(
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier
                    .fillMaxSize()
                    .verticalScroll(rememberScrollState())
            ) {
                Text(text = "Second screen")
            }
        }
    }
ChristopherKlammt commented 2 years ago

I can confirm the issue, also seeing this when navigating back from a detail screen to a screen with expanded toolbar and there the title flickers (first is small and then big), because its font size depends on progress.

Could narrow it down to the height being the initial value (maximum int value) and thus progress being near 0 (because height is in the denominator for calculating progress).

aurimas-zarskis commented 2 years ago

@onebone any update on this?

onebone commented 2 years ago

I can confirm the issue and looks like its cause is about timing as @ChristopherKlammt said. As a workaround, I added minHeight, maxHeight to the rememerSaveable and it stopped being dispatched twice. It might be relevant to the fact that the composition at the very first time doesn't dispatch textSize change twice?

ChristopherKlammt commented 2 years ago

@onebone can you share the code for that?

Edit: I looked into it and created a PR for that change, is that how you did it as well?