From f2db9a7b70847223fa43f02ddf8d63073ae14838 Mon Sep 17 00:00:00 2001 From: Henrique Velloso Date: Thu, 26 Feb 2026 19:40:40 -0300 Subject: [PATCH] hotfix(nav): fix lag and UI inconsistencies in navigation bar Fixes #333 The navigation bar felt laggy on tap, especially on mid-range devices The splash/ripple touch effect on nav items and TextButtons was contributing to the perceived delay. Additionally, the pill height changed when switching tabs, and the profile avatar border made the selected pill grow. Performance: - Remove splash, ripple, and highlight from BottomNavigationBar and NavigationRail - Apply NoSplash to TextButtons globally UI: - Use fixed pill size for all nav items so height stays consistent - Unify icon size (28px) for Search, Updates, and Profile - Add avatar border when selected using strokeAlignInside so it doesn't increase layout size --- lib/screens/main_scaffold.dart | 238 +++++++++++++++++++-------------- lib/theme.dart | 6 + 2 files changed, 143 insertions(+), 101 deletions(-) diff --git a/lib/screens/main_scaffold.dart b/lib/screens/main_scaffold.dart index 56dbd00..d6071e7 100644 --- a/lib/screens/main_scaffold.dart +++ b/lib/screens/main_scaffold.dart @@ -8,6 +8,10 @@ import 'package:zapstore/widgets/common/badges.dart'; import '../widgets/common/profile_avatar.dart'; import '../theme.dart'; +/// Fixed size for nav bar item pills so they don't grow when content changes. +const _kNavItemPillWidth = 60.0; +const _kNavItemPillHeight = 44.0; + /// Main scaffold with bottom navigation that adapts to screen size class MainScaffold extends StatelessWidget { const MainScaffold({super.key, required this.navigationShell}); @@ -152,7 +156,13 @@ class MobileScaffold extends ConsumerWidget { removeBottom: true, child: Padding( padding: const EdgeInsets.only(bottom: 8), - child: BottomNavigationBar( + child: Theme( + data: Theme.of(context).copyWith( + splashFactory: NoSplash.splashFactory, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + ), + child: BottomNavigationBar( currentIndex: navigationShell.currentIndex, type: BottomNavigationBarType.fixed, onTap: (index) => onNavTap(context, navigationShell, index), @@ -165,117 +175,123 @@ class MobileScaffold extends ConsumerWidget { context, ).colorScheme.onSurface.withValues(alpha: 0.5), selectedIconTheme: IconThemeData( - size: 26, + size: 28, color: Theme.of(context).colorScheme.primary, ), unselectedIconTheme: IconThemeData( - size: 22, + size: 28, color: Theme.of( context, ).colorScheme.onSurface.withValues(alpha: 0.5), ), items: [ BottomNavigationBarItem( - icon: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + icon: SizedBox( + width: _kNavItemPillWidth, + height: _kNavItemPillHeight, + child: Container( + decoration: navigationShell.currentIndex == 0 + ? BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(16), + ) + : null, + alignment: Alignment.center, + child: const Icon(Icons.search_rounded), ), - decoration: navigationShell.currentIndex == 0 - ? BoxDecoration( - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(16), - ) - : null, - child: const Icon(Icons.search_rounded), ), label: 'Search', ), BottomNavigationBarItem( - icon: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: navigationShell.currentIndex == 1 - ? BoxDecoration( - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(16), - ) - : null, - child: Stack( - clipBehavior: Clip.none, - children: [ - const Icon(Icons.update), - if (isLoadingUpdates) - Positioned( - right: -8, - top: -8, - child: BadgePill( - child: const SizedBox( - width: 8, - height: 8, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, + icon: SizedBox( + width: _kNavItemPillWidth, + height: _kNavItemPillHeight, + child: Container( + decoration: navigationShell.currentIndex == 1 + ? BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(16), + ) + : null, + alignment: Alignment.center, + child: Stack( + clipBehavior: Clip.none, + children: [ + const Icon(Icons.update), + if (isLoadingUpdates) + Positioned( + right: -8, + top: -8, + child: BadgePill( + child: const SizedBox( + width: 8, + height: 8, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), ), ), + ) + else if (updateCount > 0) + Positioned( + right: -8, + top: -8, + child: CountBadge(count: updateCount), ), - ) - else if (updateCount > 0) - Positioned( - right: -8, - top: -8, - child: CountBadge(count: updateCount), - ), - ], + ], + ), ), ), label: 'Updates', ), BottomNavigationBarItem( - icon: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + icon: SizedBox( + width: _kNavItemPillWidth, + height: _kNavItemPillHeight, + child: Container( + decoration: navigationShell.currentIndex == 2 + ? BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(16), + ) + : null, + alignment: Alignment.center, + child: pubkey != null + ? Container( + decoration: navigationShell.currentIndex == 2 + ? BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Theme.of( + context, + ).colorScheme.primary, + width: 2, + strokeAlign: + BorderSide.strokeAlignInside, + ), + ) + : null, + child: ProfileAvatar( + profile: profile, + pubkey: pubkey, + radius: 14, + ), + ) + : const Icon(Icons.person_rounded), ), - decoration: navigationShell.currentIndex == 2 - ? BoxDecoration( - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(16), - ) - : null, - child: pubkey != null - ? Container( - decoration: navigationShell.currentIndex == 2 - ? BoxDecoration( - border: Border.all( - color: Theme.of( - context, - ).colorScheme.primary, - width: 2, - ), - borderRadius: BorderRadius.circular(16), - ) - : null, - child: ProfileAvatar( - profile: profile, - pubkey: pubkey, - radius: 14, - ), - ) - : const Icon(Icons.person_rounded), ), label: 'Profile', ), ], ), + ), ), ), ), @@ -331,12 +347,28 @@ class DesktopScaffold extends ConsumerWidget { ), ), ), - child: NavigationRail( - backgroundColor: Colors.transparent, - selectedIndex: navigationShell.currentIndex, - onDestinationSelected: (index) => - onNavTap(context, navigationShell, index), - labelType: NavigationRailLabelType.all, + child: Theme( + data: Theme.of(context).copyWith( + splashFactory: NoSplash.splashFactory, + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + ), + child: NavigationRail( + backgroundColor: Colors.transparent, + selectedIndex: navigationShell.currentIndex, + onDestinationSelected: (index) => + onNavTap(context, navigationShell, index), + selectedIconTheme: IconThemeData( + size: 28, + color: Theme.of(context).colorScheme.primary, + ), + unselectedIconTheme: IconThemeData( + size: 28, + color: Theme.of(context) + .colorScheme.onSurface + .withValues(alpha: 0.5), + ), + labelType: NavigationRailLabelType.all, destinations: [ const NavigationRailDestination( icon: Icon(Icons.search), @@ -375,17 +407,20 @@ class DesktopScaffold extends ConsumerWidget { NavigationRailDestination( icon: pubkey != null ? Container( - decoration: navigationShell.currentIndex == 2 - ? BoxDecoration( - border: Border.all( - color: Theme.of( - context, - ).colorScheme.primary, - width: 2, - ), - borderRadius: BorderRadius.circular(16), - ) - : null, + decoration: + navigationShell.currentIndex == 2 + ? BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Theme.of( + context, + ).colorScheme.primary, + width: 2, + strokeAlign: + BorderSide.strokeAlignInside, + ), + ) + : null, child: ProfileAvatar( profile: profile, pubkey: pubkey, @@ -397,6 +432,7 @@ class DesktopScaffold extends ConsumerWidget { ), ], ), + ), ), Expanded(child: navigationShell), ], diff --git a/lib/theme.dart b/lib/theme.dart index 789a4b3..5bf314a 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -308,6 +308,12 @@ final darkTheme = ThemeData( ), ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + splashFactory: NoSplash.splashFactory, + ), + ), + // Professional dark input design inputDecorationTheme: InputDecorationTheme( border: OutlineInputBorder(