Stelios Papamichail
Stelios Papamichail

Reputation: 1290

Adding spacing between BottomNavigationItem's icon & label

I'm trying to build out my Bottom navigation like this:

@Composable
fun BottomNavBar(navController: NavController) {
    Column(
        Modifier.background(colorResource(id = R.color.pastel_orange_white))
    ) {
        BottomNavigation(
            modifier = Modifier
                .defaultMinSize(minHeight = 70.dp),
            backgroundColor = colorResource(id = R.color.bottom_nav_dark)
        ) {
            val navItems = arrayOf(
                BottomNavItem(
                    stringResource(id = R.string.title_home),
                    R.drawable.ic_home,
                    Screen.Home.route
                ),
                BottomNavItem(
                    stringResource(id = R.string.subjects_title),
                    R.drawable.ic_subject,
                    Screen.Subjects.route
                ),
                BottomNavItem(
                    stringResource(id = R.string.grades_title),
                    R.drawable.ic_grade,
                    Screen.Grades.route
                ),
                BottomNavItem(
                    "H/W",
                    R.drawable.ic_assignments,
                    Screen.Assignments.route
                )
            )
            // observe the backstack
            val navBackStackEntry by navController.currentBackStackEntryAsState()
            // observe current route to change the icon
            // color,label color when navigated
            val currentRoute = navBackStackEntry?.destination?.route
            navItems.forEach { navItem ->
                BottomNavigationItem(
                    selected = currentRoute == navItem.route,
                    onClick = {
                        navController.navigate(navItem.route)
                    },
                    icon = {
                        Box(
                            Modifier
                                .width(70.dp)
                                .height(30.dp)
                                .background(
                                    colorResource(id = if (currentRoute == navItem.route) R.color.bottom_nav_light else R.color.bottom_nav_dark),
                                    RoundedCornerShape(32.dp)
                                ),
                            contentAlignment = Alignment.Center
                        ) {
                            Icon(
                                painter = painterResource(id = navItem.icon),
                                contentDescription = navItem.label,
                                tint = colorResource(id = R.color.black)
                            )
                        }
                    },
                    label = {
                        Text(text = navItem.label, fontSize = 14.sp)
                    },
                    alwaysShowLabel = true,
                    selectedContentColor = colorResource(id = R.color.black),
                    unselectedContentColor = colorResource(id = R.color.black)
                )
            }
        }
    }
}

I need to add some extra space between the label and the icon parts, since I'm applying a small background color to the selected item. I tried with paddings, column arrangements, etc. but didn't find something that actually affected the spacing. Any pointers?

Upvotes: 3

Views: 3018

Answers (4)

william xyz
william xyz

Reputation: 1041

One simple solution would be using Modifier.offset to y axis. Like this:

...
icon = {
    Icon(
        modifier = Modifier.offset(y = (-4).dp),
        painter = painterResource(icon),
        contentDescription = stringResource(description),
    )
}
...

In my case -4.dp worked well, but you may adjust it for your needs.

Upvotes: 0

yousef Abbdolzadeh
yousef Abbdolzadeh

Reputation: 420

Add padding to your icon

     items.forEach { item ->
        BottomNavigationItem(
            icon = {
                Icon(
                    modifier=Modifier.padding(6.dp), //add this line
                    painter = painterResource(id = item.icon),
                    tint = MaterialTheme.colorScheme.onSurface,
                    contentDescription = stringResource(id = item.title)
                )
            },

            label = {
                Text(
                     text = stringResource(id = item.title),
                    style = MaterialTheme.typography.labelSmall,
                    color = MaterialTheme.colorScheme.onSurface
                )
            },
            selected = currentRoute == item.route,
            
        )
    }

Upvotes: 3

R0ck
R0ck

Reputation: 499

I managed to add padding between the icon and the label. In fact, when I was trying this initially, I was applying the padding to the text/label and not the icon, but if we apply padding to the icon, it is possible to apply that spacing. Just add a Modifier.padding(bottom = YOURDP) to your Icon and it will work.

I think this is possible because, after analyzing the Material Design code to apply the Label with Icon, it applies the Label relative to Icon (you can see that on the method below), considering the existing space, so if we increase the space that Icon occupies, it will so that the label is lower.

/**
 * Places the provided [labelPlaceable] and [iconPlaceable] in the correct position, depending on
 * [iconPositionAnimationProgress].
 *
 * When [iconPositionAnimationProgress] is 0, [iconPlaceable] will be placed in the center, as with
 * [placeIcon], and [labelPlaceable] will not be shown.
 *
 * When [iconPositionAnimationProgress] is 1, [iconPlaceable] will be placed near the top of item,
 * and [labelPlaceable] will be placed at the bottom of the item, according to the spec.
 *
 * When [iconPositionAnimationProgress] is animating between these values, [iconPlaceable] will be
 * placed at an interpolated position between its centered position and final resting position.
 *
 * @param labelPlaceable text label placeable inside this item
 * @param iconPlaceable icon placeable inside this item
 * @param constraints constraints of the item
 * @param iconPositionAnimationProgress the progress of the icon position animation, where 0
 * represents centered icon and no label, and 1 represents top aligned icon with label.
 * Values between 0 and 1 interpolate the icon position so we can smoothly move the icon.
 */
private fun MeasureScope.placeLabelAndIcon(
    labelPlaceable: Placeable,
    iconPlaceable: Placeable,
    constraints: Constraints,
    /*@FloatRange(from = 0.0, to = 1.0)*/
    iconPositionAnimationProgress: Float
): MeasureResult {
    val height = constraints.maxHeight

    // TODO: consider multiple lines of text here, not really supported by spec but we should
    // have a better strategy than overlapping the icon and label
    val baseline = labelPlaceable[LastBaseline]

    val baselineOffset = CombinedItemTextBaseline.roundToPx()

    // Label should be [baselineOffset] from the bottom
    val labelY = height - baseline - baselineOffset

    val unselectedIconY = (height - iconPlaceable.height) / 2

    // Icon should be [baselineOffset] from the text baseline, which is itself
    // [baselineOffset] from the bottom
    val selectedIconY = height - (baselineOffset * 2) - iconPlaceable.height

    val containerWidth = max(labelPlaceable.width, iconPlaceable.width)

    val labelX = (containerWidth - labelPlaceable.width) / 2
    val iconX = (containerWidth - iconPlaceable.width) / 2

    // How far the icon needs to move between unselected and selected states
    val iconDistance = unselectedIconY - selectedIconY

    // When selected the icon is above the unselected position, so we will animate moving
    // downwards from the selected state, so when progress is 1, the total distance is 0, and we
    // are at the selected state.
    val offset = (iconDistance * (1 - iconPositionAnimationProgress)).roundToInt()

    return layout(containerWidth, height) {
        if (iconPositionAnimationProgress != 0f) {
            labelPlaceable.placeRelative(labelX, labelY + offset)
        }
        iconPlaceable.placeRelative(iconX, selectedIconY + offset)
    }
}

Upvotes: 0

nglauber
nglauber

Reputation: 24024

I don't think you can do it using a BottomNavigationItem. The Material Design Library for Compose follows the Material Design Specification as you can see here.

But you don't need to use a BottomNavigationItem to display the items in your BottomAppBar. You can use any composable, like below:

Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
        .weight(1f)
        .clickable {
            navController.navigate(navItem.route)
        }) {
    val color =
        if (currentRoute == navItem.route) 
            LocalContentColor.current 
        else 
            LocalContentColor.current.copy(alpha = ContentAlpha.medium)
    Icon(
        painter = painterResource(id = navItem.icon), 
        navItem.label, 
        tint = color
    )
    // Set the space that you want...
    Spacer(modifier = Modifier.height(4.dp))
    Text(text = navItem.label, color = color, fontSize = 12.sp)
}

However, as I mentioned above, the library follows the Material Design specs, which define the BottomAppBar's height as 56dp. Therefore, if you want something more custom, you need to it by your own (using a Row, for instance).

Upvotes: 4

Related Questions