mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-28 23:08:59 +02:00
fix(shell): rail overflow on short viewports and tab reset on rotation
- NavigationRail scrolls inside its SafeArea when the viewport is too short for four labeled destinations (landscape phone with the mini player), instead of overflowing by its natural height - the PageView subtree carries a GlobalKey so swapping the body between rail and bottom-bar layouts at the 600dp breakpoint moves the element instead of remounting it - rotation no longer snaps back to the home tab and kept-alive tab state survives
This commit is contained in:
+61
-38
@@ -42,6 +42,9 @@ class MainShell extends ConsumerStatefulWidget {
|
||||
class _MainShellState extends ConsumerState<MainShell>
|
||||
with SingleTickerProviderStateMixin {
|
||||
int _currentIndex = 0;
|
||||
// Preserves the PageView element (and its kept-alive tabs) when the body
|
||||
// structure swaps between rail and bottom-bar layouts on rotation.
|
||||
final GlobalKey _pageViewKey = GlobalKey();
|
||||
late final PageController _pageController;
|
||||
late final AnimationController _tabJumpTransitionController;
|
||||
bool _hasCheckedUpdate = false;
|
||||
@@ -588,33 +591,36 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
// bottom NavigationBar on phones.
|
||||
final useNavigationRail = MediaQuery.sizeOf(context).width >= 600;
|
||||
|
||||
final pageView = AnimatedBuilder(
|
||||
animation: _tabJumpTransitionController,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: tabs.length,
|
||||
onPageChanged: _onPageChanged,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// TickerMode mutes animations and lets visibility-aware widgets
|
||||
// (e.g. MotionHeaderBanner) pause when their tab is hidden —
|
||||
// kept-alive pages otherwise keep running offscreen.
|
||||
itemBuilder: (context, index) => _KeepAliveTabPage(
|
||||
key: ValueKey('page-$index'),
|
||||
child: TickerMode(
|
||||
enabled: index == _currentIndex,
|
||||
child: tabs[index],
|
||||
final pageView = KeyedSubtree(
|
||||
key: _pageViewKey,
|
||||
child: AnimatedBuilder(
|
||||
animation: _tabJumpTransitionController,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: tabs.length,
|
||||
onPageChanged: _onPageChanged,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// TickerMode mutes animations and lets visibility-aware widgets
|
||||
// (e.g. MotionHeaderBanner) pause when their tab is hidden —
|
||||
// kept-alive pages otherwise keep running offscreen.
|
||||
itemBuilder: (context, index) => _KeepAliveTabPage(
|
||||
key: ValueKey('page-$index'),
|
||||
child: TickerMode(
|
||||
enabled: index == _currentIndex,
|
||||
child: tabs[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
builder: (context, child) {
|
||||
final t = Curves.easeOutCubic.transform(
|
||||
_tabJumpTransitionController.value,
|
||||
);
|
||||
return Opacity(
|
||||
opacity: t,
|
||||
child: Transform.scale(scale: 0.985 + (0.015 * t), child: child),
|
||||
);
|
||||
},
|
||||
),
|
||||
builder: (context, child) {
|
||||
final t = Curves.easeOutCubic.transform(
|
||||
_tabJumpTransitionController.value,
|
||||
);
|
||||
return Opacity(
|
||||
opacity: t,
|
||||
child: Transform.scale(scale: 0.985 + (0.015 * t), child: child),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return BackButtonListener(
|
||||
@@ -624,27 +630,44 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
},
|
||||
child: Scaffold(
|
||||
extendBody: true,
|
||||
// The page view keeps one element across the rail<->bar structure
|
||||
// swap via _pageViewKey; without it a rotation past the 600dp
|
||||
// breakpoint remounts the PageView and snaps back to the first tab.
|
||||
body: useNavigationRail
|
||||
? Row(
|
||||
children: [
|
||||
SafeArea(
|
||||
right: false,
|
||||
bottom: false,
|
||||
child: NavigationRail(
|
||||
selectedIndex: _currentIndex.clamp(0, maxIndex),
|
||||
onDestinationSelected: _onNavTap,
|
||||
labelType: NavigationRailLabelType.all,
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
destinations: [
|
||||
for (final destination in destinations)
|
||||
NavigationRailDestination(
|
||||
icon: destination.icon,
|
||||
selectedIcon: destination.selectedIcon,
|
||||
label: Text(destination.label),
|
||||
// The rail needs ~300dp of height for four labeled
|
||||
// destinations; on short viewports (landscape phone with
|
||||
// the mini player showing) it must scroll, not overflow.
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
],
|
||||
child: IntrinsicHeight(
|
||||
child: NavigationRail(
|
||||
selectedIndex: _currentIndex.clamp(0, maxIndex),
|
||||
onDestinationSelected: _onNavTap,
|
||||
labelType: NavigationRailLabelType.all,
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
destinations: [
|
||||
for (final destination in destinations)
|
||||
NavigationRailDestination(
|
||||
icon: destination.icon,
|
||||
selectedIcon: destination.selectedIcon,
|
||||
label: Text(destination.label),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
|
||||
Reference in New Issue
Block a user