androidandroid-jetpack-composenavigationbarbottom-navigation-bar

Can we have a NavigationBar instance per screen without functional disadvantages?


When using Jetpack Compose and Material Design, is it possible to use a new NavigationBar (aka BottomNavigationBar) on each screen? Or would this interrupt e.g. touch animations as a new NavigationBar gets instantiated when switching screens?

My idea behind this is that this would really make it easy to hide or show the NavigationBar on some screens and not on others (instead of communicating e.g. with a Scaffold higher up in the hierarchy)


Solution

  • There is nothing special about BottomNavigation composable as you can see in its source code below

    @Composable
    fun BottomNavigation(
        modifier: Modifier = Modifier,
        backgroundColor: Color = MaterialTheme.colors.primarySurface,
        contentColor: Color = contentColorFor(backgroundColor),
        elevation: Dp = BottomNavigationDefaults.Elevation,
        content: @Composable RowScope.() -> Unit
    ) {
        Surface(
            color = backgroundColor,
            contentColor = contentColor,
            elevation = elevation,
            modifier = modifier
        ) {
            Row(
                Modifier
                    .fillMaxWidth()
                    .height(BottomNavigationHeight)
                    .selectableGroup(),
                horizontalArrangement = Arrangement.SpaceBetween,
                content = content
            )
        }
    }
    

    calling BottomNavigation on each screen is similar to using another Composable on any screen. Also, BottomNavigationItem is just one Box or two Boxes when both label and icon is present with progress for animating alpha and label offset.

    @Composable
    fun RowScope.BottomNavigationItem(
        selected: Boolean,
        onClick: () -> Unit,
        icon: @Composable () -> Unit,
        modifier: Modifier = Modifier,
        enabled: Boolean = true,
        label: @Composable (() -> Unit)? = null,
        alwaysShowLabel: Boolean = true,
        interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
        selectedContentColor: Color = LocalContentColor.current,
        unselectedContentColor: Color = selectedContentColor.copy(alpha = ContentAlpha.medium)
    ) {
        val styledLabel: @Composable (() -> Unit)? = label?.let {
            @Composable {
                val style = MaterialTheme.typography.caption.copy(textAlign = TextAlign.Center)
                ProvideTextStyle(style, content = label)
            }
        }
        // The color of the Ripple should always the selected color, as we want to show the color
        // before the item is considered selected, and hence before the new contentColor is
        // provided by BottomNavigationTransition.
        val ripple = rememberRipple(bounded = false, color = selectedContentColor)
    
        Box(
            modifier
                .selectable(
                    selected = selected,
                    onClick = onClick,
                    enabled = enabled,
                    role = Role.Tab,
                    interactionSource = interactionSource,
                    indication = ripple
                )
                .weight(1f),
            contentAlignment = Alignment.Center
        ) {
            BottomNavigationTransition(
                selectedContentColor,
                unselectedContentColor,
                selected
            ) { progress ->
                val animationProgress = if (alwaysShowLabel) 1f else progress
    
                BottomNavigationItemBaselineLayout(
                    icon = icon,
                    label = styledLabel,
                    iconPositionAnimationProgress = animationProgress
                )
            }
        }
    }
    

    Or would this interrupt e.g. touch animations as a new NavigationBar gets instantiated when switching screens?

    It depends on which kind of animation that is invoked since new BottomNavigation composable will be called on each screen when that screen enters composition.

    If you have an animation that start when an item is clicked and takes a second to complete for instance, on new page you will likely to see target index as selected in target page while you might want this animation to continue as it was only on a single page.

    Animation Progress

    BottomNavigationItem gets progress value from transition as

    @Composable
    private fun BottomNavigationTransition(
        activeColor: Color,
        inactiveColor: Color,
        selected: Boolean,
        content: @Composable (animationProgress: Float) -> Unit
    ) {
        val animationProgress by animateFloatAsState(
            targetValue = if (selected) 1f else 0f,
            animationSpec = BottomNavigationAnimationSpec
        )
    
        val color = lerp(inactiveColor, activeColor, animationProgress)
    
        CompositionLocalProvider(
            LocalContentColor provides color.copy(alpha = 1f),
            LocalContentAlpha provides color.alpha,
        ) {
            content(animationProgress)
        }
    }
    

    You can create you Composable like this to animate progress since writing you own BottomNavigationItem is straightforward and simple.

    You can refer this answer about synching animations basically it's passing a progress value to multiple Composables if they don't enter composition at the same time.