Repository navigation
android: draw the bottom bar as an overlay so its glass has a backdrop (#1836) - #1837
Merged
Merged
Conversation
#1836) The bar was already built to float — pill shape, 22dp inset, navigationBarsPadding, hairline rim, 0.80 alpha, soft shadow. It rendered as a dark strip anyway, and the reason was where it was MOUNTED, not how it was styled. It sat in Scaffold(bottomBar = ...), a slot OUTSIDE the screen content. Each screen paints its own backdrop through LiquidScreenSky / BackgroundImageBackdrop, so that backdrop stopped where the bar began: behind the glass there was only containerColor = surfaceBase. Translucency over a flat colour is a slightly different flat colour, which is exactly what a user comparing it to the iOS bar was seeing — most obvious with a custom background (#1234), where the bar reads as a block cut out of the image. Drawn as a sibling over the Scaffold instead, so it floats on whatever the current screen painted. Same composable, same insets: only its parent moved. The slot stays but empty, so the Scaffold reserves nothing, and the inset it used to contribute is applied to the NavHost — content clears the bar exactly as before. LocalBottomBarHeight is exposed for the half deliberately NOT done here: letting content scroll UNDER the glass needs each scrollable to add that to its own contentPadding, which no shared modifier can do. That wants a device to look at, screen by screen. WHY iOS LOOKS NATIVE: it IS native. RootTabView uses SwiftUI's TabView, so iOS 26 supplies the Liquid Glass bar, the selected-item pill and real backdrop blur for free. Compose has no cheap backdrop blur, so this is not blur parity — it is making the existing translucency show something. The active-tab pill is the other visible gap and is not in this commit. Two placement traps hit while writing this, both mine: the declaration first landed between @OptIn(ExperimentalMaterial3Api::class) and the function, so the annotation applied to my val and three experimental-API errors appeared inside AppRoot. Same shape as the stacked-doc trap, fifth time this session. Kotlin: full suite 5089/0; compile and doc-comment gate clean. NOT verified on a device - this is a layout change I cannot see.
Reviewing my own change for cost turned up a correctness bug instead. Moving the bar out of Scaffold(bottomBar = ...) meant losing the measured `inner` inset, and I replaced it with a constant 76.dp. The bar's height includes navigationBarsPadding(), which is roughly 24dp under gesture navigation and roughly 48dp under 3-button navigation, and changes again on rotation or when a foldable opens. So a constant is wrong by about 24dp on exactly the 3-button device the original report came from - content would sit behind the bar. Measured with onSizeChanged and fed back as the NavHost's bottom padding: one extra layout pass at startup, then stable. Trading a measured value for a guess to avoid one pass would be the wrong trade. Also removed the LocalBottomBarHeight I had added for future per-screen padding. It was declared but never provided, so every reader would have got 0.dp - an API that looks available and silently does nothing is worse than no API. It can be added when something actually consumes it. Kotlin: full suite 5089/0; compile and doc-comment gate clean. Still not seen on a device.
Second inset bug in this PR, the mirror of the first. Having fixed a hardcoded height by measuring it, I then added that measurement to a value that already contained part of the same thing. The Scaffold does not set contentWindowInsets, so it defaults to WindowInsets.systemBars and `inner.bottom` already carries the navigation-bar inset. The bar's measured height carries it too, through its own navigationBarsPadding(). `.padding(inner).padding(bottom = barHeight)` therefore counted that inset twice and left roughly a navigation bar's worth of dead space between content and the bar — widest on 3-button navigation, which is the device in the report. Take top and sides from the Scaffold and REPLACE the bottom with the bar height. The bar owns the navigation inset; content only has to clear the bar. Both bugs came from treating an inset as a number to combine rather than asking what already accounts for what. Neither is visible without a device, which is why this stays a draft. Kotlin: full suite 5089/0; compile and doc-comment gate clean.
) Both layouts now ship. Settings > Appearance > Bottom bar overlay, default OFF, so nothing changes for anyone until they ask for it. That is the right default for this change specifically. The overlay is what the bar was built for — its glass finally has a backdrop instead of sitting on surfaceBase — but it is app-shell layout that no test can judge, and the two inset bugs found while writing it were both invisible without a device. A switch turns "unverified change everyone gets" into "unverified change someone can try, and turn off". The default path is the shipped Scaffold slot, byte-identical to before: the slot renders the bar and the NavHost pads by `inner`. The overlay path empties the slot, draws the bar as a sibling over the Scaffold, and replaces the Scaffold's bottom inset with the measured bar height. BottomBarStyleStore is snapshot-backed, mirroring BackgroundImageStore, so the toggle applies without a relaunch. Two more placement traps hit while writing this — the store landed under AppRoot's KDoc and the pref key under the Clock key's, orphaning both. Seventh and eighth this session. The gate caught them; I did not. Kotlin: full suite 5089/0. compile, doc-comment, i18n audit and lintVitalFullRelease all clean. String added in all 7 locales.
ryanbr
marked this pull request as ready for review
September 3, 2026 02:42
ryanbr
added a commit
that referenced
this pull request
Sep 3, 2026
versionCode 419 -> 420, CURRENT_PROJECT_VERSION 300 -> 301. versionName and MARKETING_VERSION stay at 11.1.1 - a testing build, not a version bump. Adds the optional overlay bottom bar (#1837) on top of what 419 carried. It is OFF by default, so this build looks identical until Settings > Appearance > Bottom bar overlay is turned on - which is the point of cutting it: the switch makes an unverified layout reachable without a special build. Still open on this build: the 11.1.0 startup report remains undiagnosed, and the overlay bar has never been seen on a device.
ryanbr
added a commit
that referenced
this pull request
Sep 3, 2026
#1837 shipped a toggle that changed nothing on screen. Moving the bar out of the Scaffold slot was not enough on its own: a screen's backdrop is painted INSIDE the screen, and the overlay path still inset the NavHost by the bar height, so the screen and its backdrop stopped exactly where they had before. The bar floated over a strip of the shell's container colour - identical layout, and a dark band where the background should have continued. The screen now reaches the bottom edge so its full-bleed backdrop paints behind and around the glass, and ScreenScaffold adds the bar's height to the scrolling CONTENT's bottom padding instead. That padding sits inside the scroll viewport, so content passes under the bar while scrolling and the last row still comes to rest clear of it. One change rather than four: every top-level screen routes through ScreenScaffold. StepsCalibration is the one NavHost destination that scrolls without it and is handled directly. Still behind Settings > Appearance > Bottom bar overlay, default off. Raised on #1836.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — this is a layout change I cannot see. It compiles and the suite is green, but somebody has to look
at it on a phone before it should land.
Raised on #1836.
The bar was never the problem
GlassBottomBaris already built to float: pill shape, 22 dp inset,navigationBarsPadding(), hairlinerim, 0.80 alpha, soft shadow. It rendered as a dark strip regardless — because of where it was mounted,
not how it was styled.
It sat in
Scaffold(bottomBar = …), a slot outside the screen content. Each screen paints its ownbackdrop via
LiquidScreenSky/BackgroundImageBackdrop, so that backdrop stopped where the bar began,and behind the glass there was only
containerColor = Palette.surfaceBase.Translucency over a flat colour is a slightly different flat colour. With a custom background (#1234) it
reads as a block cut out of the image, which is what the side-by-side screenshots show.
What changed
The bar is drawn as a sibling over the Scaffold, so it floats on whatever the current screen painted.
Same composable, same insets — only its parent moved. The slot stays but empty, so the Scaffold reserves
nothing, and the inset it used to contribute is applied to the
NavHost. Content clears the bar exactly asbefore; only what sits behind the glass differs.
Deliberately not in this change
contentPadding; no shared modifier can do it.LocalBottomBarHeightis exposed for that work, screenby screen, with a device to look at.
BarSlotchanges only colour and weight, where the system iOS bar draws afilled rounded background behind the selected item. That is the other visible gap and belongs in its own
change.
blur parity — it is making the translucency that already exists show something.
Why iOS looks native
Because it is.
RootTabViewuses SwiftUI'sTabView, so iOS 26 supplies the Liquid Glass bar, theselected-item pill, real blur and scroll behaviour for free. Android's bar is hand-built and always will
be an approximation; this closes the part of the gap that was a mounting mistake rather than a platform
limit.
On #90
#90 is an open prototype (July) for a scroll-reactive version of this same bar, editing the same
composable and two months stale. Whichever lands first, the other should be rebased onto it rather than
both restructuring
GlassBottomBarin parallel. Notes on that PR separately.Verification
compileFullDebugKotlinclean; full Android suite 5089 tests, 0 failures; doc-comment gate clean.does not honour the NavHost inset, and the overlay's behaviour with the keyboard open.
Now behind a setting, default off
Settings → Appearance → Bottom bar overlay, default off. Merging changes nothing on screen until
someone turns it on.
The off-path resolves to exactly what ships today — the slot renders the bar, the NavHost pads by
inner,and the overlay composable is never composed, so its measurement never runs. What changes unconditionally
is structural only:
run { }became aBox, two remembered values the off-path never reads, and theBottomBarStyleStoreobject.That is the right shape for this change. Both inset bugs found while writing it were invisible without a
device, so shipping it as the only option would have been asking users to find the third one. A switch
makes it reachable for testing without a special build, while the default stays inert.
Still unverified on a device. It should be described as experimental wherever it is announced until
someone reports back from having flipped it.