Skip to content

PROTOTYPE: scroll-reactive bottom bar (Instagram-style) (#86) - #90

Closed
ryanbr wants to merge 6 commits into
mainfrom
prototype-scroll-navbar
Closed

ryanbr wants to merge 6 commits into
mainfrom
prototype-scroll-navbar

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Prototype for #86 (Quaii) — a subtle, scroll-reactive bottom bar like Instagram's.

Behaviour

The floating GlassBottomBar recedes (a subtle shrink + fade + slide toward the bottom edge) as you scroll down into content, and returns when you scroll up — so the app feels more dynamic without hiding the tabs. Kept partial by design (scale ~0.88, alpha to 0.6, translateY 22dp) so the tabs stay reachable — "subtle but noticeable," per the request.

How it's wired (built for smooth scroll)

  • One NestedScrollConnection on the NavHost drives it — every screen gets the behaviour with no per-screen wiring (scrollable children dispatch their deltas up to it).
  • onPreScroll only flips a Boolean on a direction change; a 220ms tween animates a single collapse float.
  • The bar applies collapse as a graphicsLayer transform only (GPU, per-frame) — no relayout, because the Scaffold keeps the bar's reserved space. So the content doesn't reflow and scrolling stays smooth.
  • Reduce Motion → bar stays fully shown (no animation).

Scope / testing

  • Android only for now (its bar is a single custom Surface, easy + low-risk). If the feel lands, I'll port to the iOS floating two-island bar + FAB.
  • The transform constants (shrink/fade/slide amounts, 220ms, 3px threshold) are the tuning knobs — easy to dial once you see it.
  • Android compiles locally (compileFullDebugKotlin, exit 0).

⚠️ I can't see the linked sample video (can't fetch the attachment), so this is my read of "subtle Instagram-style size change." If you want it to fully hide on scroll (more like the classic Instagram tab bar) rather than just recede, that's a one-line change to the translationY/alpha — tell me which feel matches the video.

File: android/app/src/main/java/com/noop/ui/AppRoot.kt.

ryanbr added 4 commits July 8, 2026 23:51
… on up (#86)

Instagram-style: the floating GlassBottomBar subtly recedes (shrink + fade + slide
toward the edge) as you scroll DOWN into content and returns when you scroll UP,
so the app feels more dynamic without hiding the tabs.

- One NestedScrollConnection on the NavHost drives it, so every screen gets the
  behaviour with zero per-screen wiring. onPreScroll only flips a Boolean on a
  direction change; a 220ms tween animates a single `collapse` float.
- The bar applies `collapse` as a graphicsLayer transform ONLY (scale ~0.88, alpha
  to 0.6, translateY 22dp, bottom-centre pivot). No relayout — the Scaffold keeps
  the bar's reserved space — so scrolling stays smooth (GPU transform per frame).
- Reduce Motion keeps the bar fully shown (no animation). Subtle by design; the
  transform constants are the tuning knobs. Android only for now.

Android compiles locally.
…e off (#86)

Feedback: the collapsed bar slid too far down (into the gesture-nav area) and
didn't read as transparent. Retuned: fade toward see-through (alpha to ~0.35),
gentler shrink (0.92), and only a hair of drift (22dp -> 6dp) so it recedes IN
PLACE rather than falling off the bottom. Android compiles.
…tabs (#86)

Feedback (correctly, twice): the pill's dark translucent fill read as a solid
"bar" behind the tabs and blocked the content. Drop the fill entirely — the pill
is now just the hairline capsule outline + tab icons floating over the content,
which shows straight through. Also removed the drop shadow (also read as a slab).
Android compiles.
…" band (#86)

The full-width band behind the pill was the Scaffold's RESERVED bottomBar region
(filled with the flat app background), sitting below the tinted content. Moved the
bar out of the bottomBar slot and overlay it (Box + align bottom-centre) on
full-bleed content, so the content's background fills continuously behind the
now-transparent pill — no band. NestedScroll moves to the full-bleed NavHost.

Note: content now scrolls UNDER the floating transparent pill (iOS-style); screens
keep their 28dp bottom padding, so the last item can sit partly behind the bar at
the very bottom of a scroll — a bottom-padding bump is the follow-up if that bites.
Android compiles.

@tigercraft4 tigercraft4 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prototype feedback (non-blocking) — the graphicsLayer-only recede is the right perf call and Reduce Motion is respected. Two things that would bite before this leaves prototype, plus a jitter nit.

// #86: NO fill. The dark translucent fill read as a solid "bar" behind the tabs; the content
// should show straight through. Keep the pill SHAPE via the hairline outline only (a floating
// capsule of tabs over the content), no fill and no drop shadow (which also read as a slab).
color = Color.Transparent,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legibility. Dropping the fill to Color.Transparent + removing the shadow means the tab icons/labels now sit directly over arbitrary content (charts, bright cards) with only a hairline outline behind them. On busy/light backgrounds the tabs can wash out. The iOS FloatingTabBar keeps a translucent material for exactly this — suggest keeping a light scrim/blur (or a subtle fill) rather than fully transparent.

modifier = Modifier.padding(inner),
// #86: full-bleed so content (and its background) fills behind the floating bar. The scroll
// direction is observed here (one place → every screen) to drive the bar recede.
modifier = Modifier.fillMaxSize().nestedScroll(barNestedScroll),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bar is no longer a reserved bottomBar slot, so content now scrolls the full height with the pill floating over it. Unless each screen adds bottom content padding (≈ pill height + inset), the last list item can be hidden behind the pill. Worth threading a contentPadding through the screens (or a shared bottom spacer).

override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (!reduceMotion) {
// available.y < 0 = content moving up (finger swipes up, reading down) → collapse.
if (available.y < -3f) barCollapsed = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: toggling on raw available.y at a ±3px threshold is jittery — a tiny scroll or fling settle can flip the bar. Consider a small accumulated-delta threshold or debouncing the collapse/expand transition.

@tigercraft4

Copy link
Copy Markdown

Heads up: #354 implements the same scroll-reactive bottom bar for #86 — both iOS and Android, plus a user-configurable behavior setting (Reactive/Expanded/Compact) and StrandDesign motion tokens. Worth comparing before landing either, so the two don't collide or duplicate work.

Quaii and others added 2 commits July 14, 2026 11:23
#354)

* Scroll-reactive bottom navigation — hybrid collapse with Reactive / Expanded / Compact setting (#86)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix: preserve bar collapse state across tab/route switches (#86)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix NestedScrollSource.UserInput → Drag for Compose BOM 2024.06 (#86)

UserInput was added in Compose 1.7 (BOM 2024.09+); Drag is the
pre-1.7 equivalent for active user-gesture scrolls and compiles
cleanly against the current BOM 2024.06.00.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract the two pure decisions from AppRoot's inline collapse composition into
testable top-level functions — isBarEffectivelyCollapsed() and
resolveBarCollapse() — and add BottomBarBehaviorTest. Behavior-preserving: the
composable now calls the extracted functions instead of the inline if/else chain.

Covers storage-string resolution (unknown/null -> REACTIVE, round-trip), the
COMPACT-always / EXPANDED-never / REACTIVE-follows-flag collapse decision, and
the fraction composition (Reduce Motion snaps to 0/1, non-reactive rides the
settle, reactive-collapsed takes max(precompression, settle), reactive-expanding
follows precompression then settle). 9 tests, testFullDebugUnitTest green.

Follows up #354: that PR left the Android-unit-tests box unchecked; this is the
test gate before the feature promotes from prototype-scroll-navbar to main.
ryanbr added a commit that referenced this pull request Sep 3, 2026
Settings > Appearance > Hide bar when scrolling. Default off, and unavailable unless the overlay bar
is on - in the slot layout the Scaffold has reserved the bar's space, so hiding it would leave an
empty band rather than give the space back.

One NestedScrollConnection on the shell, so every screen inherits the behaviour with no per-screen
wiring. The bar moves by graphicsLayer only - GPU, no relayout - and the animating float is read
inside the layer block rather than in composition, so the shell is not recomposed sixty times a
second while the bar moves. Presence is a derivedStateOf, so the bar leaves the tree once hidden and
cannot be focused by TalkBack or catch a tap it should not.

Reduce Motion keeps the bar visible rather than snapping it away, and the toggle greys out to say so
instead of accepting a tap it cannot act on. A route change resets it, so a screen never opens with
its navigation hidden.

The shape is from the #90 prototype - one connection, transform-only, decision in tested plain
functions. Raised on #1839.
@ryanbr

ryanbr commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

Retiring this: the prototype shipped. bottomBarAutoHide is in main as a stored setting, a toggle in
Settings, and .noopTabBarAutoHide(...) applied in RootTabView, with the Android side in
MainActivity. Nothing here is unmerged work.

Verified directly rather than from the audit in #2059, whose evidence line for this one happened to
hold but whose method does not establish that on its own.

@ryanbr ryanbr closed this Sep 10, 2026
@github-actions
github-actions Bot deleted the prototype-scroll-navbar branch September 14, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants