compose-collapsing-toolbar icon indicating copy to clipboard operation
compose-collapsing-toolbar copied to clipboard

CollapsingToolbarScaffoldState.toolbarState.progress changes without scrolling

Open aurimas-zarskis opened this issue 3 years ago • 6 comments

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.

aurimas-zarskis avatar Jan 13 '22 14:01 aurimas-zarskis

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

onebone avatar Jan 15 '22 14:01 onebone

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")
            }
        }
    }

aurimas-zarskis avatar Jan 17 '22 09:01 aurimas-zarskis

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).

ChristopherKlammt avatar Jan 17 '22 12:01 ChristopherKlammt

@onebone any update on this?

aurimas-zarskis avatar Feb 23 '22 21:02 aurimas-zarskis

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?

onebone avatar Feb 24 '22 14:02 onebone

@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?

ChristopherKlammt avatar Mar 11 '22 17:03 ChristopherKlammt