diff --git a/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift b/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift index 84081fa5..3245007f 100644 --- a/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift +++ b/Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift @@ -158,8 +158,9 @@ private func flexibleLayoutFloat(_ value: CGFloat?) -> Float? { /// the given closure as a pixel rect. /// - Parameter checkEdges: Which edges to check to see if we're against a safe area. Any matching edges will be /// passed to the given closure. +/// - Parameter bottomAdjacencyTolerancePx: Distance from the bottom safe boundary still considered adjacent. /// - Parameter logTag: When non-empty, emits Android ``Log`` lines with tag `SkipUI.ISAL.` (e.g. filter logcat `SkipUI.ISAL.List`). -@Composable func IgnoresSafeAreaLayout(expandInto: Edge.Set, checkEdges: Edge.Set = [], modifier: Modifier = Modifier, logTag: String = "", target: @Composable (IntRect, Edge.Set) -> Void) { +@Composable func IgnoresSafeAreaLayout(expandInto: Edge.Set, checkEdges: Edge.Set = [], modifier: Modifier = Modifier, bottomAdjacencyTolerancePx: Float = Float(0.1), logTag: String = "", target: @Composable (IntRect, Edge.Set) -> Void) { guard let safeArea = EnvironmentValues.shared._safeArea else { if !logTag.isEmpty { Log.d("SkipUI.ISAL.\(logTag)", "no SafeArea in environment; skipping expansion") @@ -220,7 +221,7 @@ private func flexibleLayoutFloat(_ value: CGFloat?) -> Float? { } in: { Layout(modifier: modifier.onGloballyPositionedInWindow { let probeEdges = expandInto.union(checkEdges) - let newEdges = adjacentSafeAreaEdges(bounds: $0, safeArea: safeArea, isRTL: isRTL, checkEdges: probeEdges) + let newEdges = adjacentSafeAreaEdges(bounds: $0, safeArea: safeArea, isRTL: isRTL, checkEdges: probeEdges, bottomTolerancePx: bottomAdjacencyTolerancePx) if !logTag.isEmpty { let previous = edgesState.value if newEdges != previous { @@ -255,12 +256,12 @@ private func flexibleLayoutFloat(_ value: CGFloat?) -> Float? { } } -private func adjacentSafeAreaEdges(bounds: Rect, safeArea: SafeArea, isRTL: Bool, checkEdges: Edge.Set) -> Edge.Set { +private func adjacentSafeAreaEdges(bounds: Rect, safeArea: SafeArea, isRTL: Bool, checkEdges: Edge.Set, bottomTolerancePx: Float = Float(0.1)) -> Edge.Set { var edges: Edge.Set = [] if checkEdges.contains(Edge.Set.top), bounds.top <= safeArea.safeBoundsPx.top + 0.1 { edges.insert(Edge.Set.top) } - if checkEdges.contains(Edge.Set.bottom), bounds.bottom >= safeArea.safeBoundsPx.bottom - 0.1 { + if checkEdges.contains(Edge.Set.bottom), bounds.bottom >= safeArea.safeBoundsPx.bottom - bottomTolerancePx { edges.insert(Edge.Set.bottom) } if isRTL { diff --git a/Sources/SkipUI/SkipUI/Containers/List.swift b/Sources/SkipUI/SkipUI/Containers/List.swift index e7d029fc..562f6841 100644 --- a/Sources/SkipUI/SkipUI/Containers/List.swift +++ b/Sources/SkipUI/SkipUI/Containers/List.swift @@ -156,8 +156,9 @@ public final class List : View, Renderable { let safeArea = EnvironmentValues.shared._safeArea var ignoresSafeAreaEdges: Edge.Set = [.top, .bottom] ignoresSafeAreaEdges.formIntersection(safeArea?.absoluteSystemBarEdges ?? []) + let bottomAdjacencyTolerancePx = safeArea.map { max(Float(0.1), $0.presentationBoundsPx.bottom - $0.safeBoundsPx.bottom) } ?? Float(0.1) ComposeContainer(scrollAxes: .vertical, modifier: context.modifier, fillWidth: true, fillHeight: true, then: Modifier.background(BackgroundColor(styling: styling, isItem: false))) { modifier in - IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: [.bottom], modifier: modifier, logTag: "List") { safeAreaExpansion, safeAreaEdges in + IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: [.bottom], modifier: modifier, bottomAdjacencyTolerancePx: bottomAdjacencyTolerancePx, logTag: "List") { safeAreaExpansion, safeAreaEdges in var containerModifier: Modifier let refreshing = remember { mutableStateOf(false) } let refreshAction = EnvironmentValues.shared.refresh diff --git a/Sources/SkipUI/SkipUI/Containers/Navigation.swift b/Sources/SkipUI/SkipUI/Containers/Navigation.swift index 10f79c23..50e68aff 100644 --- a/Sources/SkipUI/SkipUI/Containers/Navigation.swift +++ b/Sources/SkipUI/SkipUI/Containers/Navigation.swift @@ -76,6 +76,7 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController @@ -165,7 +166,8 @@ public struct NavigationStack : View, Renderable { // When we layout, only extend into safe areas that are due to system bars, not into any app chrome var ignoresSafeAreaEdges: Edge.Set = [.top, .bottom] ignoresSafeAreaEdges.formIntersection(safeArea?.absoluteSystemBarEdges ?? []) - IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: ignoresSafeAreaEdges, logTag: "NavigationStack") { _, _ in + let bottomAdjacencyTolerancePx = safeArea.map { max(Float(0.1), $0.presentationBoundsPx.bottom - $0.safeBoundsPx.bottom) } ?? Float(0.1) + IgnoresSafeAreaLayout(expandInto: ignoresSafeAreaEdges, checkEdges: ignoresSafeAreaEdges, bottomAdjacencyTolerancePx: bottomAdjacencyTolerancePx, logTag: "NavigationStack") { _, _ in ComposeContainer(modifier: context.modifier, fillWidth: true, fillHeight: true) { modifier in let decoratorList = listOf(rememberSaveableStateHolderNavEntryDecorator()) let entryProvider = entryProvider { @@ -260,7 +262,10 @@ public struct NavigationStack : View, Renderable { let topBarPreferences = arguments.toolbarPreferences.navigationBar let bottomBarPreferences = arguments.toolbarPreferences.bottomBar - let effectiveTitleDisplayMode = navigator.value.titleDisplayMode(for: state, hasTitle: hasTitle, preference: titleDisplayPreference) + let presentationTopPx = arguments.safeArea?.presentationBoundsPx.top ?? Float(0.0) + let isInitialRootPreferencePass = remember { mutableStateOf(arguments.isRoot) } + let reservesInitialRootTopBar = isInitialRootPreferencePass.value && arguments.isRoot && !hasTitle && (topBarPreferences?.visibility ?? Visibility.automatic) == Visibility.automatic + let effectiveTitleDisplayMode = navigator.value.titleDisplayMode(for: state, hasTitle: hasTitle || reservesInitialRootTopBar, preference: titleDisplayPreference) let isInlineTitleDisplayMode = useInlineTitleDisplayMode(for: effectiveTitleDisplayMode, safeArea: arguments.safeArea) // We would like to only process toolbar content in our topBar/bottomBar Composables, but composing @@ -309,31 +314,43 @@ public struct NavigationStack : View, Renderable { navigationIconButtonColors = nil } var modifier = Modifier.nestedScroll(searchFieldScrollConnection) - if showTopBar { + SideEffect { + if isInitialRootPreferencePass.value { + isInitialRootPreferencePass.value = false + } + } + let showsTopBar = showTopBar || reservesInitialRootTopBar + if showsTopBar { modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) } modifier = modifier.then(context.modifier) - let defaultTopBarHeight = 112.dp - let topBarBottomPx = remember { - let safeAreaTopPx = arguments.safeArea?.safeBoundsPx.top ?? Float(0.0) - // Use a first-frame estimate only when a top bar will render. The measured value from - // onGloballyPositionedInWindow below is the source of truth after composition. - mutableStateOf(showTopBar ? with(density) { safeAreaTopPx + defaultTopBarHeight.toPx() } : Float(0.0)) - } - let topBarHeightPx = remember { - mutableStateOf(showTopBar ? with(density) { defaultTopBarHeight.toPx() } : Float(0.0)) - } - // Reactively clear the reserved inset whenever the bar is hidden — covers both a - // title-less root (where AnimatedVisibility's onDispose never runs) and visible→hidden - // transitions where onDispose may not have fired yet. - LaunchedEffect(showTopBar) { - if !showTopBar { + let shouldReserveInitialTopBarSpace = showTopBar || reservesInitialRootTopBar + let estimatedCollapsedTopBarHeight = 64.dp + let estimatedExpandedTopBarHeight = 112.dp + let estimatedTopBarHeight = isInlineTitleDisplayMode ? estimatedCollapsedTopBarHeight : estimatedExpandedTopBarHeight + let safeAreaTopPx = arguments.safeArea?.safeBoundsPx.top ?? Float(0.0) + let hasAbsoluteTopSystemBar = arguments.safeArea?.absoluteSystemBarEdges.contains(.top) == true + let estimatedTopBarBottomPx = shouldReserveInitialTopBarSpace ? with(density) { safeAreaTopPx + estimatedTopBarHeight.toPx() } : Float(0.0) + let estimatedTopBarBottomInPresentationPx = max(Float(0.0), estimatedTopBarBottomPx - presentationTopPx) + let topBarBottomPx = remember { mutableStateOf(estimatedTopBarBottomInPresentationPx) } + let measuredTopBarIsInline = remember { mutableStateOf(isInlineTitleDisplayMode) } + let measuredTopBarSafeAreaTopPx = remember { mutableStateOf(nil) } + let measuredTopBarPresentationTopPx = remember { mutableStateOf(nil) } + LaunchedEffect(showsTopBar, isInitialRootPreferencePass.value) { + if !showsTopBar && !isInitialRootPreferencePass.value { topBarBottomPx.value = Float(0.0) - topBarHeightPx.value = Float(0.0) + measuredTopBarSafeAreaTopPx.value = nil + measuredTopBarPresentationTopPx.value = nil } } - + var safeAreaMatchesMeasuredTopBar = false + if let measuredSafeAreaTopPx = measuredTopBarSafeAreaTopPx.value, let measuredPresentationTopPx = measuredTopBarPresentationTopPx.value { + safeAreaMatchesMeasuredTopBar = measuredSafeAreaTopPx == safeAreaTopPx && measuredPresentationTopPx == presentationTopPx + } + let isPresentedAsSheet = presentationTopPx > Float(0.0) && arguments.safeArea?.absoluteSystemBarEdges.contains(.top) != true + let hasCurrentTopBarMeasurement = measuredTopBarIsInline.value == isInlineTitleDisplayMode && (isPresentedAsSheet || safeAreaMatchesMeasuredTopBar) && topBarBottomPx.value > Float(0.0) + let effectiveTopBarBottomPx = shouldReserveInitialTopBarSpace ? presentationTopPx + (hasCurrentTopBarMeasurement ? topBarBottomPx.value : estimatedTopBarBottomInPresentationPx) : Float(0.0) let isSystemBackground = topBarPreferences?.isSystemBackground == true let topBar: @Composable () -> Void = { let animation: Animation = Animation.current(isAnimating: false) ?? topBarPreferences?.visibilityAnimation ?? Animation.linear(duration: 0) @@ -341,11 +358,12 @@ public struct NavigationStack : View, Renderable { let moveEdgeTop = MoveTransition(edge: .top) let topBarEnter = moveEdgeTop.asEnterTransition(spec: animationSpec) let topBarExit = moveEdgeTop.asExitTransition(spec: animationSpec) - AnimatedVisibility(visible: showTopBar, modifier: Modifier.fillMaxWidth(), enter: topBarEnter, exit: topBarExit, label: "NavigationTopBar") { + AnimatedVisibility(visible: showsTopBar, modifier: Modifier.fillMaxWidth(), enter: topBarEnter, exit: topBarExit, label: "NavigationTopBar") { DisposableEffect(true) { onDispose { topBarBottomPx.value = Float(0.0) - topBarHeightPx.value = Float(0.0) + measuredTopBarSafeAreaTopPx.value = nil + measuredTopBarPresentationTopPx.value = nil } } let isOverlapped = scrollBehavior.state.overlappedFraction > 0 @@ -362,7 +380,7 @@ public struct NavigationStack : View, Renderable { // If there is a custom color scheme, we also always show any custom background even when unscrolled, because we can't // properly interpolate between the title text colors let topBarHasColorScheme = topBarPreferences?.colorScheme != nil - let isSystemBackground = topBarPreferences?.isSystemBackground == true + let isSystemBackground = topBarPreferences?.isSystemBackground == true || reservesInitialRootTopBar if topBarPreferences?.backgroundVisibility == Visibility.hidden { topBarBackgroundColor = androidx.compose.ui.graphics.Color.Transparent unscrolledTopBarBackgroundColor = androidx.compose.ui.graphics.Color.Transparent @@ -395,9 +413,16 @@ public struct NavigationStack : View, Renderable { .clickable(interactionSource: interactionSource, indication: nil, onClick: { scrollToTop.value.reduced.action() }) - .onGloballyPositionedInWindow { bounds in - topBarBottomPx.value = bounds.bottom - topBarHeightPx.value = bounds.bottom - bounds.top + .onGloballyPositioned { coordinates in + let heightPx = Float(coordinates.size.height) + if heightPx > Float(0.0) { + // Toolbar item content can measure taller in some locales. Keep the content inset + // tied to the standard app bar height so navigation transitions stay stable. + topBarBottomPx.value = min(heightPx, estimatedTopBarBottomInPresentationPx) + measuredTopBarIsInline.value = isInlineTitleDisplayMode + } + measuredTopBarSafeAreaTopPx.value = safeAreaTopPx + measuredTopBarPresentationTopPx.value = presentationTopPx } if !topBarHasColorScheme || isOverlapped, let topBarBackgroundForBrush { let opacity = topBarHasColorScheme ? 1.0 : isInlineTitleDisplayMode ? min(1.0, Double(scrollBehavior.state.overlappedFraction * 5)) : Double(scrollBehavior.state.collapsedFraction) @@ -471,11 +496,12 @@ public struct NavigationStack : View, Renderable { } // Use scrollBehavior (from the early call) for the TopAppBar to ensure it matches the nestedScrollConnection options = options.copy(scrollBehavior: scrollBehavior) + let topBarWindowInsets = hasAbsoluteTopSystemBar ? TopAppBarDefaults.windowInsets : WindowInsets(0.dp, 0.dp, 0.dp, 0.dp) if isInlineTitleDisplayMode { if options.preferCenterAlignedStyle { - CenterAlignedTopAppBar(title: options.title, modifier: options.modifier, navigationIcon: options.navigationIcon, actions: { topBarActions() }, colors: options.colors, scrollBehavior: options.scrollBehavior) + CenterAlignedTopAppBar(title: options.title, modifier: options.modifier, navigationIcon: options.navigationIcon, actions: { topBarActions() }, colors: options.colors, scrollBehavior: options.scrollBehavior, windowInsets: topBarWindowInsets) } else { - TopAppBar(title: options.title, modifier: options.modifier, navigationIcon: options.navigationIcon, actions: { topBarActions() }, colors: options.colors, scrollBehavior: options.scrollBehavior) + TopAppBar(title: options.title, modifier: options.modifier, navigationIcon: options.navigationIcon, actions: { topBarActions() }, colors: options.colors, scrollBehavior: options.scrollBehavior, windowInsets: topBarWindowInsets) } } else { // Force a larger, bold title style in the uncollapsed state by replacing the headlineSmall style the bar uses @@ -484,9 +510,9 @@ public struct NavigationStack : View, Renderable { let appBarTypography = typography.copy(headlineSmall: appBarTitleStyle) MaterialTheme(colorScheme: MaterialTheme.colorScheme, typography: appBarTypography, shapes: MaterialTheme.shapes) { if options.preferLargeStyle { - LargeTopAppBar(title: options.title, modifier: options.modifier, navigationIcon: options.navigationIcon, actions: { topBarActions() }, colors: options.colors, scrollBehavior: options.scrollBehavior) + LargeTopAppBar(title: options.title, modifier: options.modifier, navigationIcon: options.navigationIcon, actions: { topBarActions() }, colors: options.colors, scrollBehavior: options.scrollBehavior, windowInsets: topBarWindowInsets) } else { - MediumTopAppBar(title: options.title, modifier: options.modifier, navigationIcon: options.navigationIcon, actions: { topBarActions() }, colors: options.colors, scrollBehavior: options.scrollBehavior) + MediumTopAppBar(title: options.title, modifier: options.modifier, navigationIcon: options.navigationIcon, actions: { topBarActions() }, colors: options.colors, scrollBehavior: options.scrollBehavior, windowInsets: topBarWindowInsets) } } } @@ -566,7 +592,8 @@ public struct NavigationStack : View, Renderable { let bottomPadding = with(density) { min(bottomBarHeightPx.value, Float(WindowInsets.ime.getBottom(density))).toDp() } PaddingLayout(padding: EdgeInsets(top: 0.0, leading: 0.0, bottom: Double(-bottomPadding.value), trailing: 0.0), context: context.content()) { context in let containerColor = showScrolledBackground ? bottomBarBackgroundColor : unscrolledBottomBarBackgroundColor - let windowInsets = EnvironmentValues.shared._isEdgeToEdge == true ? BottomAppBarDefaults.windowInsets : WindowInsets(bottom: 0.dp) + let usesBottomSystemBarInset = EnvironmentValues.shared._isEdgeToEdge == true && arguments.safeArea?.absoluteSystemBarEdges.contains(.bottom) == true + let windowInsets = usesBottomSystemBarInset ? BottomAppBarDefaults.windowInsets : WindowInsets(bottom: 0.dp) var options = Material3BottomAppBarOptions(modifier: context.modifier.then(bottomBarModifier), containerColor: containerColor, contentColor: MaterialTheme.colorScheme.contentColorFor(containerColor), contentPadding: PaddingValues.Absolute(left: 16.dp, right: 16.dp)) if let updateOptions = EnvironmentValues.shared._material3BottomAppBar { options = updateOptions(options) @@ -594,10 +621,10 @@ public struct NavigationStack : View, Renderable { Column(modifier: modifier.background(Color.background.colorImpl())) { // Calculate safe area for content let contentSafeArea = arguments.safeArea? - .insetting(.top, to: topBarBottomPx.value) + .insetting(.top, to: effectiveTopBarBottomPx) .insetting(.bottom, to: bottomBarTopPx.value) // Inset manually for any edge where our container ignored the safe area, but we aren't showing a bar - let topPadding = topBarBottomPx.value <= Float(0.0) && arguments.ignoresSafeAreaEdges.contains(.top) ? WindowInsets.safeDrawing.asPaddingValues().calculateTopPadding() : 0.dp + let topPadding = effectiveTopBarBottomPx <= Float(0.0) && arguments.ignoresSafeAreaEdges.contains(.top) ? WindowInsets.safeDrawing.asPaddingValues().calculateTopPadding() : 0.dp var bottomPadding = 0.dp if bottomBarTopPx.value <= Float(0.0) && arguments.ignoresSafeAreaEdges.contains(.bottom) { bottomPadding = max(0.dp, WindowInsets.safeDrawing.asPaddingValues().calculateBottomPadding() - WindowInsets.ime.asPaddingValues().calculateBottomPadding()) @@ -648,17 +675,17 @@ public struct NavigationStack : View, Renderable { // Calculate safe area for content by insetting by topBar and bottomBar heights var contentSafeArea: SafeArea? if let safeArea = arguments.safeArea { - let clampedTopBarBottomPxValue: Float = max(topBarBottomPx.value, safeArea.safeBoundsPx.top) + let clampedTopBarBottomPxValue: Float = max(effectiveTopBarBottomPx, safeArea.safeBoundsPx.top) contentSafeArea = safeArea .insetting(.top, to: clampedTopBarBottomPxValue) .insetting(.bottom, to: bottomBarTopPx.value) } - + // Top bar aligned to top Box(modifier: Modifier.zIndex(Float(1.1)).align(androidx.compose.ui.Alignment.TopCenter)) { topBar() } - + // Bottom bar aligned to bottom Box(modifier: Modifier.zIndex(Float(1.1)).align(androidx.compose.ui.Alignment.BottomCenter)) { bottomBar() @@ -672,10 +699,74 @@ public struct NavigationStack : View, Renderable { // indicator. var contentModifier = Modifier.fillMaxSize() let safeTopDp = WindowInsets.safeDrawing.asPaddingValues().calculateTopPadding() - let topBarHeightDp = with(density) { topBarHeightPx.value.toDp() } - let topPadding = arguments.ignoresSafeAreaEdges.contains(.top) ? max(topBarHeightDp, safeTopDp) : topBarHeightDp - let bottomPadding = bottomBarHeightPx.value <= Float(0.0) && arguments.ignoresSafeAreaEdges.contains(.bottom) ? - max(0.dp, WindowInsets.safeDrawing.asPaddingValues().calculateBottomPadding() - WindowInsets.ime.asPaddingValues().calculateBottomPadding()) : with(density) { bottomBarHeightPx.value.toDp() } + let topBarBottomDp: Dp + if showsTopBar { + if isInlineTitleDisplayMode { + let localTopBarBottomPx = max(Float(0.0), effectiveTopBarBottomPx - (arguments.safeArea?.presentationBoundsPx.top ?? Float(0.0))) + topBarBottomDp = with(density) { localTopBarBottomPx.toDp() } + } else { + let localSafeAreaTopDp = with(density) { max(Float(0.0), safeAreaTopPx - presentationTopPx).toDp() } + let topBarHeightDp = estimatedCollapsedTopBarHeight + ((estimatedExpandedTopBarHeight - estimatedCollapsedTopBarHeight) * Float(1.0 - scrollBehavior.state.collapsedFraction)) + topBarBottomDp = localSafeAreaTopDp + topBarHeightDp + } + } else { + topBarBottomDp = 0.dp + } + + let topPadding = arguments.ignoresSafeAreaEdges.contains(.top) ? max(topBarBottomDp, safeTopDp) : topBarBottomDp + let bottomPadding: Dp + + let topBarUnderlayColor: androidx.compose.ui.graphics.Color? + if topBarPreferences?.backgroundVisibility == Visibility.hidden { + topBarUnderlayColor = nil + } else if topBarPreferences?.backgroundVisibility == Visibility.visible { + topBarUnderlayColor = topBarPreferences?.background?.asColor(opacity: 1.0, animationContext: nil) ?? Color.systemBarBackground.colorImpl() + } else if topBarPreferences?.isSystemBackground == true || reservesInitialRootTopBar { + topBarUnderlayColor = Color.systemBarBackground.colorImpl() + } else { + topBarUnderlayColor = nil + } + + if showsTopBar && topPadding.value > Float(0.0), let topBarUnderlayColor { + Box( + modifier: Modifier + .zIndex(Float(1.0)) + .align(androidx.compose.ui.Alignment.TopCenter) + .fillMaxWidth() + .height(topPadding) + .background(topBarUnderlayColor) + ) {} + } + + if bottomBarHeightPx.value > Float(0.0) { + bottomPadding = with(density) { bottomBarHeightPx.value.toDp() } + } else if arguments.ignoresSafeAreaEdges.contains(.bottom) { + bottomPadding = max(0.dp, WindowInsets.safeDrawing.asPaddingValues().calculateBottomPadding() - WindowInsets.ime.asPaddingValues().calculateBottomPadding()) + } else { + bottomPadding = 0.dp + } + + let bottomBarUnderlayColor: androidx.compose.ui.graphics.Color? + if bottomBarPreferences?.backgroundVisibility == Visibility.hidden { + bottomBarUnderlayColor = nil + } else if bottomBarPreferences?.backgroundVisibility == Visibility.visible { + bottomBarUnderlayColor = bottomBarPreferences?.background?.asColor(opacity: 1.0, animationContext: nil) ?? Color.systemBarBackground.colorImpl() + } else if bottomBarPreferences?.isSystemBackground == true { + bottomBarUnderlayColor = Color.systemBarBackground.colorImpl() + } else { + bottomBarUnderlayColor = nil + } + + if bottomPadding.value > Float(0.0), let bottomBarUnderlayColor { + Box( + modifier: Modifier + .align(androidx.compose.ui.Alignment.BottomCenter) + .fillMaxWidth() + .height(bottomPadding) + .background(bottomBarUnderlayColor) + ) {} + } + contentModifier = contentModifier.padding(top: topPadding, bottom: bottomPadding) Box(modifier: contentModifier, contentAlignment: androidx.compose.ui.Alignment.Center) { var topPadding = 0.dp