Search code examples
androidandroid-jetpack-composeandroid-jetpack-compose-material3

Where in the Material 3 TopAppBar code is the padding defined?


I know that the TopAppBar in Material 3 has a Left/right padding of 16dp. However I am wondering where in the source code that is defined? For example, the TopAppBar constructor looks like this - no hint to the default left/right padding.

@ExperimentalMaterial3Api
@Composable
fun TopAppBar(
    title: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    navigationIcon: @Composable () -> Unit = {},
    actions: @Composable RowScope.() -> Unit = {},
    windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
    colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
    scrollBehavior: TopAppBarScrollBehavior? = null
) {
    SingleRowTopAppBar(
        modifier = modifier,
        title = title,
        titleTextStyle = MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont),
        centeredTitle = false,
        navigationIcon = navigationIcon,
        actions = actions,
        windowInsets = windowInsets,
        colors = colors,
        scrollBehavior = scrollBehavior
    )
}

Is there is any constant I can use to apply this padding to other components or do I have to hardcode the 16dp? I want the things below the TopAppBar to have the same left/right padding, otherwise it does not look consistent.


Solution

  • It is defined further down the call chain inside the TopAppBarLayout composable, inside the titlePlaceable.placeRelative section.

    Here are the relevant code sections

    Box(
        Modifier
            .layoutId("navigationIcon")
            .padding(start = TopAppBarHorizontalPadding) // <-- line of interest
    ) {
        CompositionLocalProvider(
            LocalContentColor provides navigationIconContentColor,
            content = navigationIcon
        )
    }
    Box(
        Modifier
            .layoutId("title")
            .padding(horizontal = TopAppBarHorizontalPadding) // <-- line of interest
            // ...
    ) {
        ProvideTextStyle(value = titleTextStyle) {
            CompositionLocalProvider(
                LocalContentColor provides titleContentColor.copy(alpha = titleAlpha),
                content = title
            )
        }
    }  
    
    layout(constraints.maxWidth, layoutHeight) {
        // Navigation icon
        navigationIconPlaceable.placeRelative(
            x = 0,
            y = (layoutHeight - navigationIconPlaceable.height) / 2
        )
        // Title
        titlePlaceable.placeRelative(
            x = when (titleHorizontalArrangement) {
                Arrangement.Center -> (constraints.maxWidth - titlePlaceable.width) / 2
                Arrangement.End ->
                    constraints.maxWidth - titlePlaceable.width - actionIconsPlaceable.width
                // Arrangement.Start.
                // An TopAppBarTitleInset will make sure the title is offset in case the
                // navigation icon is missing.
                else -> max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width)
            },
        // ...
    

    And this are the values used in the code above

    private val TopAppBarHorizontalPadding = 4.dp
    private val TopAppBarTitleInset = 16.dp - TopAppBarHorizontalPadding
    

    These are defined as part of the library so they are basically constants, and are unlikely to change between versions. Even if they ever change, you have control over when to update the library and at that point you would also update your paddings to match again.

    So when the top app bar does not display a navigation icon, the padding at the start side is 12.dp and then the title Box follows, which has its own padding of 4.dp for a total of 16.dp of padding.

    And when the top bar does display a navigation icon the padding at the start side is 0.dp then the navigation icon Box follows, which has its own padding of 4.dp for a total of 4.dp of padding. However, usually the navigation icons are added by using an IconButton and adding an Icon as content to it. The minimum (touch) size of the IconButton is 48.dp (defined inside the IconButton composable) and the default Icon size is 24.dp. This means that even though the actual padding is 4.dp the "visual" padding up to the side of the Icon is 4.dp + (48.dp / 2 - 24.dp / 2) = 4.dp + 12.dp = 16.dp.

    It is similar on the end side where the action icons are or where the title ends (when there are no action icons).

    So in both cases (as long as you are using default icon sizes) the "visual" paddings are the same at 16.dp. So if you decide to go with a padding of 16.dp for your content the layouts will look aligned.

    Note, that all this is assuming that the left/right windowInsets are 0, which might not be the case on all devices and all orientations.

    So to ensure that the layouts look aligned in all cases you also need to take windowInsets into consideration in your layout. If you are already, then things should align correctly when you add 16.dp of padding. If you are not, then you either need to apply windowInsets to you (parent) composable (this should be the easier option) or you need to add the windowInsets to the calculations for the correct side based on LayoutDirection by calling calculateLeftPadding and calculateRightPadding.

    val ld = LocalLayoutDirection.current
    val insets = windowInsets.asPaddingValues(LocalDensity.current)
    val leftPadding = insets.calculateLeftPadding(ld)
    val rightPadding = insets.calculateRightPadding(ld)