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
This commit is contained in:
Henrique Velloso
2026-02-26 19:40:40 -03:00
parent a721139064
commit f2db9a7b70
2 changed files with 143 additions and 101 deletions
+137 -101
View File
@@ -8,6 +8,10 @@ import 'package:zapstore/widgets/common/badges.dart';
import '../widgets/common/profile_avatar.dart'; import '../widgets/common/profile_avatar.dart';
import '../theme.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 /// Main scaffold with bottom navigation that adapts to screen size
class MainScaffold extends StatelessWidget { class MainScaffold extends StatelessWidget {
const MainScaffold({super.key, required this.navigationShell}); const MainScaffold({super.key, required this.navigationShell});
@@ -152,7 +156,13 @@ class MobileScaffold extends ConsumerWidget {
removeBottom: true, removeBottom: true,
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 8), 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, currentIndex: navigationShell.currentIndex,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
onTap: (index) => onNavTap(context, navigationShell, index), onTap: (index) => onNavTap(context, navigationShell, index),
@@ -165,117 +175,123 @@ class MobileScaffold extends ConsumerWidget {
context, context,
).colorScheme.onSurface.withValues(alpha: 0.5), ).colorScheme.onSurface.withValues(alpha: 0.5),
selectedIconTheme: IconThemeData( selectedIconTheme: IconThemeData(
size: 26, size: 28,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
unselectedIconTheme: IconThemeData( unselectedIconTheme: IconThemeData(
size: 22, size: 28,
color: Theme.of( color: Theme.of(
context, context,
).colorScheme.onSurface.withValues(alpha: 0.5), ).colorScheme.onSurface.withValues(alpha: 0.5),
), ),
items: [ items: [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Container( icon: SizedBox(
padding: const EdgeInsets.symmetric( width: _kNavItemPillWidth,
horizontal: 16, height: _kNavItemPillHeight,
vertical: 8, 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', label: 'Search',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Container( icon: SizedBox(
padding: const EdgeInsets.symmetric( width: _kNavItemPillWidth,
horizontal: 16, height: _kNavItemPillHeight,
vertical: 8, child: Container(
), decoration: navigationShell.currentIndex == 1
decoration: navigationShell.currentIndex == 1 ? BoxDecoration(
? BoxDecoration( color: Theme.of(
color: Theme.of( context,
context, ).colorScheme.primary.withValues(alpha: 0.15),
).colorScheme.primary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(16), )
) : null,
: null, alignment: Alignment.center,
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
const Icon(Icons.update), const Icon(Icons.update),
if (isLoadingUpdates) if (isLoadingUpdates)
Positioned( Positioned(
right: -8, right: -8,
top: -8, top: -8,
child: BadgePill( child: BadgePill(
child: const SizedBox( child: const SizedBox(
width: 8, width: 8,
height: 8, height: 8,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: Colors.white, 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', label: 'Updates',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Container( icon: SizedBox(
padding: const EdgeInsets.symmetric( width: _kNavItemPillWidth,
horizontal: 16, height: _kNavItemPillHeight,
vertical: 8, 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', label: 'Profile',
), ),
], ],
), ),
),
), ),
), ),
), ),
@@ -331,12 +347,28 @@ class DesktopScaffold extends ConsumerWidget {
), ),
), ),
), ),
child: NavigationRail( child: Theme(
backgroundColor: Colors.transparent, data: Theme.of(context).copyWith(
selectedIndex: navigationShell.currentIndex, splashFactory: NoSplash.splashFactory,
onDestinationSelected: (index) => highlightColor: Colors.transparent,
onNavTap(context, navigationShell, index), splashColor: Colors.transparent,
labelType: NavigationRailLabelType.all, ),
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: [ destinations: [
const NavigationRailDestination( const NavigationRailDestination(
icon: Icon(Icons.search), icon: Icon(Icons.search),
@@ -375,17 +407,20 @@ class DesktopScaffold extends ConsumerWidget {
NavigationRailDestination( NavigationRailDestination(
icon: pubkey != null icon: pubkey != null
? Container( ? Container(
decoration: navigationShell.currentIndex == 2 decoration:
? BoxDecoration( navigationShell.currentIndex == 2
border: Border.all( ? BoxDecoration(
color: Theme.of( shape: BoxShape.circle,
context, border: Border.all(
).colorScheme.primary, color: Theme.of(
width: 2, context,
), ).colorScheme.primary,
borderRadius: BorderRadius.circular(16), width: 2,
) strokeAlign:
: null, BorderSide.strokeAlignInside,
),
)
: null,
child: ProfileAvatar( child: ProfileAvatar(
profile: profile, profile: profile,
pubkey: pubkey, pubkey: pubkey,
@@ -397,6 +432,7 @@ class DesktopScaffold extends ConsumerWidget {
), ),
], ],
), ),
),
), ),
Expanded(child: navigationShell), Expanded(child: navigationShell),
], ],
+6
View File
@@ -308,6 +308,12 @@ final darkTheme = ThemeData(
), ),
), ),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
),
),
// Professional dark input design // Professional dark input design
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder( border: OutlineInputBorder(