Make the shell top bar hide/reveal with scroll, per tab
Each regular tab (Files/Photos/Favorites/Shares/Recent/Activity/Trash) now plants its own AppTopBar as the first sliver in its own CustomScrollView, via a new topBarSliver() helper (tab_state_slivers.dart) wrapping the framework's SliverFloatingHeader, instead of one shared instance living in Scaffold.appBar. This gives each tab's top bar Material's native floating-app-bar behavior - tied to that tab's own ScrollController - scrolling away as the list scrolls down and reappearing the moment the drag reverses, not only once scrolled all the way back to the top, which a single fixed Scaffold.appBar (or the "put it in a SliverToBoxAdapter" escape hatch NooTopBar's doc comment already mentioned) can't do. It sits above each tab's own pinned in-content sticky header (the sort/filter row, or the selection bar that replaces it), which keeps working unchanged underneath it. main.dart builds a separate, correctly-labelled AppTopBar per tab (rather than one for "whichever tab is selected") and passes it through buildAppTabView's new optional topBar parameter; it's null on desktop (which keeps its own NooToolbar) and while picking, matching the old Scaffold.appBar: pickRequest == null guard. SliverFloatingHeader sizes itself from the bar's own natural layout (like SliverToBoxAdapter) rather than a fixed extent declared up front, so NooTopBar's own internal SafeArea keeps handling the status-bar inset correctly with no extra height math needed on this side. Adds a widget test (test/widgets/tabs/tab_state_slivers_test.dart) driving a raw gesture mid-drag to verify the reveal-follows-finger behavior, since a single tester.drag() only pumps after the gesture - including release - has already finished, which doesn't exercise the live per-frame reveal this change depends on. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,20 @@ import '../views/trash_view.dart';
|
||||
/// Builds the view widget for a given [AppTab]. Shared by the main shell
|
||||
/// (for visible tabs) and the "more tabs" dropdown (for launching a tab
|
||||
/// that's currently hidden from the bottom nav bar).
|
||||
Widget buildAppTabView(AppTab tab, ScrollController controller) {
|
||||
///
|
||||
/// [topBar], when given, is that tab's own [AppTopBar] instance (built by
|
||||
/// the caller, which owns `NooLayout.navStyle`/pick-mode/search-in-bottom-
|
||||
/// bar state) - each view plants it as its own first sliver (see
|
||||
/// `topBarSliver` in `tabs/tab_state_slivers.dart`) so it scrolls away and
|
||||
/// reappears independently, tied to that tab's own `ScrollController`
|
||||
/// rather than living in the shared `Scaffold.appBar`. Null on desktop
|
||||
/// (which shows `NooToolbar` instead) and while picking (no top bar at
|
||||
/// all), matching `Scaffold.appBar`'s old `pickRequest == null` guard.
|
||||
Widget buildAppTabView(
|
||||
AppTab tab,
|
||||
ScrollController controller, {
|
||||
PreferredSizeWidget? topBar,
|
||||
}) {
|
||||
switch (tab) {
|
||||
case AppTab.files:
|
||||
// Keyed so the Files and Offline tabs (same widget type) never share
|
||||
@@ -19,24 +32,26 @@ Widget buildAppTabView(AppTab tab, ScrollController controller) {
|
||||
return FilesView(
|
||||
key: const ValueKey('files'),
|
||||
scrollController: controller,
|
||||
topBar: topBar,
|
||||
);
|
||||
case AppTab.photos:
|
||||
return PhotosView(scrollController: controller);
|
||||
return PhotosView(scrollController: controller, topBar: topBar);
|
||||
case AppTab.favorites:
|
||||
return FavoritesView(scrollController: controller);
|
||||
return FavoritesView(scrollController: controller, topBar: topBar);
|
||||
case AppTab.activity:
|
||||
return ActivityView(scrollController: controller);
|
||||
return ActivityView(scrollController: controller, topBar: topBar);
|
||||
case AppTab.trash:
|
||||
return TrashView(scrollController: controller);
|
||||
return TrashView(scrollController: controller, topBar: topBar);
|
||||
case AppTab.shares:
|
||||
return SharesView(scrollController: controller);
|
||||
return SharesView(scrollController: controller, topBar: topBar);
|
||||
case AppTab.recent:
|
||||
return RecentView(scrollController: controller);
|
||||
return RecentView(scrollController: controller, topBar: topBar);
|
||||
case AppTab.offline:
|
||||
return FilesView(
|
||||
key: const ValueKey('offline'),
|
||||
scrollController: controller,
|
||||
offline: true,
|
||||
topBar: topBar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,3 +94,29 @@ const List<Widget> tabBottomInsetSlivers = [
|
||||
SliverToBoxAdapter(child: SizedBox(height: 100)),
|
||||
SliverFillRemaining(hasScrollBody: false, child: SizedBox()),
|
||||
];
|
||||
|
||||
/// Wraps a tab's shell top bar ([AppTopBar], passed in as the generic
|
||||
/// [PreferredSizeWidget] it implements - this file can't import
|
||||
/// `app_top_bar.dart` without a cycle) as that tab's own first sliver,
|
||||
/// living inside its `CustomScrollView` instead of `Scaffold.appBar`. Gives
|
||||
/// it Material's native "floating app bar" behavior, via the framework's own
|
||||
/// [SliverFloatingHeader]: it scrolls away as the list scrolls down, and -
|
||||
/// unlike a plain `SliverToBoxAdapter`, which only reappears once scrolled
|
||||
/// all the way back to the top - reappears immediately on any upward
|
||||
/// scroll, following the finger while dragging and settling fully open or
|
||||
/// fully closed once the gesture ends.
|
||||
///
|
||||
/// [SliverFloatingHeader] sizes itself from [topBar]'s own natural layout
|
||||
/// (like `SliverToBoxAdapter`) rather than a fixed extent declared up
|
||||
/// front - so [topBar]'s own internal `SafeArea` (see `NooTopBar`'s doc
|
||||
/// comment) already accounts for the status-bar inset correctly, with no
|
||||
/// extra height math needed here (unlike building this on the general-
|
||||
/// purpose `SliverPersistentHeader` would have required).
|
||||
///
|
||||
/// Sits above a tab's own pinned in-content header (built with
|
||||
/// [StickyHeaderDelegate] - the sort/filter controls row, or the selection
|
||||
/// bar that replaces it) - put this sliver first in `contentSlivers` so
|
||||
/// that header stays exactly where it already is, independent of whether
|
||||
/// [topBar] is currently shown or scrolled away.
|
||||
Widget topBarSliver(PreferredSizeWidget topBar) =>
|
||||
SliverFloatingHeader(child: topBar);
|
||||
|
||||
Reference in New Issue
Block a user