From de20ce8cfca2232b0fe61b22bde1c416cd8d73e7 Mon Sep 17 00:00:00 2001 From: Marc Gallert Date: Wed, 3 Sep 2025 15:24:16 +0200 Subject: [PATCH 1/6] Nav Bar with only 5 items which moves to the side; refactored padding --- lib/pages/home/widgets/bottom_nav_bar.dart | 223 +++++++++++++----- .../home/widgets/bottom_nav_bar_item.dart | 45 ++-- 2 files changed, 182 insertions(+), 86 deletions(-) diff --git a/lib/pages/home/widgets/bottom_nav_bar.dart b/lib/pages/home/widgets/bottom_nav_bar.dart index a17f98c..82c8402 100644 --- a/lib/pages/home/widgets/bottom_nav_bar.dart +++ b/lib/pages/home/widgets/bottom_nav_bar.dart @@ -26,11 +26,56 @@ class BottomNavBar extends StatefulWidget { } class _BottomNavBarState extends State { + int _prevShift = 0; + + @override + void didUpdateWidget(covariant BottomNavBar oldWidget) { + super.didUpdateWidget(oldWidget); + // prevShift will be updated in build after computing desiredShift. Keep + // previous value so AnimatedSwitcher transition direction can be derived. + // No-op here: _prevShift is updated in build to avoid extra setState. + } @override Widget build(BuildContext context) { + // Minimal change approach: render all items in a fixed-width row but clip to + // show only 5 slots. Animate a horizontal translation so one end icon slides + // off-screen depending on the active page. + // Slightly reduce horizontal padding to avoid accidental right overflow + final horizontalPadding = 6.0; + final visibleCount = 5; + final items = [ + PageItem.feed, + PageItem.events, + PageItem.mensa, + PageItem.navigation, + PageItem.wallet, + PageItem.more, + ]; + + final totalItems = items.length; + final maxShift = (totalItems - visibleCount).clamp(0, totalItems); + final activeIndex = items.indexOf(widget.currentPage); + // Aim to keep the active item roughly centered when possible; because + // totalItems-visibleCount == 1 here, shift will be 0 or 1 which matches the + // requested behavior: when on first page show first 5, when on last show last 5. + final desiredShift = (activeIndex - 2).clamp(0, maxShift).toInt(); + + + // Compute height based on platform base and device bottom inset to avoid + // overflow when system navigation/home bars reduce available height. + final bottomInset = MediaQuery.of(context).padding.bottom; + // include bottom inset so the nav bar occupies enough height above system UI + // Reduce base heights slightly to make the navigation bar more compact. + final baseHeight = Platform.isIOS ? 76.0 : 86.0; + // Use the device bottom inset but avoid adding extra slack so the bar is + // noticeably shorter while still respecting safe areas. + final containerHeight = (baseHeight + bottomInset).clamp(baseHeight, baseHeight + 64.0); + return Container( - height: Platform.isIOS ? 88 : 98, - padding: Platform.isIOS ? const EdgeInsets.only(bottom: 20, left: 5) : const EdgeInsets.only(left: 7), + height: containerHeight, + // remove the explicit bottom padding which reduced inner space and + // caused bottom overflow; keep only horizontal left padding + padding: Platform.isIOS ? const EdgeInsets.only(left: 5) : const EdgeInsets.only(left: 7), decoration: BoxDecoration( color: Provider.of(context).currentThemeData.cardColor, borderRadius: const BorderRadius.only( @@ -45,67 +90,119 @@ class _BottomNavBarState extends State { ), ], ), - child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // News Feed - BottomNavBarItem( - title: 'Feed', - imagePathActive: 'assets/img/icons/home-filled.png', - imagePathInactive: 'assets/img/icons/home-outlined.png', - onTap: () => widget.onSelectedPage(PageItem.feed), - isActive: widget.currentPage == PageItem.feed, - iconPaddingLeft: 0, - ), - // Calendar - BottomNavBarItem( - title: 'Events', - imagePathActive: 'assets/img/icons/calendar-filled.png', - imagePathInactive: 'assets/img/icons/calendar-outlined.png', - onTap: () => widget.onSelectedPage(PageItem.events), - isActive: widget.currentPage == PageItem.events, - iconPaddingLeft: 14, - ), - // Mensa - BottomNavBarItem( - title: 'Mensa', - imagePathActive: 'assets/img/icons/mensa-filled.png', - imagePathInactive: 'assets/img/icons/mensa-outlined.png', - onTap: () => widget.onSelectedPage(PageItem.mensa), - isActive: widget.currentPage == PageItem.mensa, - ), - // Navigation - BottomNavBarItem( - title: 'Navigation', - imagePathActive: 'assets/img/icons/map-filled.png', - imagePathInactive: 'assets/img/icons/map-outlined.png', - onTap: () => widget.onSelectedPage(PageItem.navigation), - isActive: widget.currentPage == PageItem.navigation, - ), - // Wallet - BottomNavBarItem( - title: 'Wallet', - imagePathActive: 'assets/img/icons/wallet-filled.png', - imagePathInactive: 'assets/img/icons/wallet-outlined.png', - onTap: () => widget.onSelectedPage(PageItem.wallet), - isActive: widget.currentPage == PageItem.wallet, - ), - // More - BottomNavBarItem( - title: 'Mehr', - imagePathActive: 'assets/img/icons/more.png', - imagePathInactive: 'assets/img/icons/more.png', - onTap: () => widget.onSelectedPage(PageItem.more), - isActive: widget.currentPage == PageItem.more, - iconPaddingLeft: 5, - iconPaddingRight: 0, - ), - ], + child: Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: ClipRect( + child: LayoutBuilder( + builder: (context, constraints) { + final navHeight = constraints.maxHeight; + final effectiveContainerWidth = constraints.maxWidth; + final computedSlotWidth = effectiveContainerWidth / visibleCount; + // computedTotalRowWidth and animatedLeft no longer needed because + // we render only the visible slice via AnimatedSwitcher. + + return SizedBox( + height: navHeight, + width: effectiveContainerWidth, + child: SizedBox( + height: navHeight, + width: effectiveContainerWidth, + child: Builder(builder: (context) { + // Determine which slice of items to show (no off-screen items) + final startIndex = desiredShift; + final visibleItems = items.sublist(startIndex, startIndex + visibleCount); + + // Decide animation direction based on previous shift + final animateForward = desiredShift >= _prevShift; + // Update prevShift for next frame + _prevShift = desiredShift; + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + final offsetAnimation = Tween( + begin: Offset(animateForward ? 1.0 : -1.0, 0), + end: Offset.zero, + ).animate(animation); + return SlideTransition(position: offsetAnimation, child: child); + }, + child: SizedBox( + width: effectiveContainerWidth, + child: Row( + key: ValueKey(desiredShift), + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + for (final p in visibleItems) + SizedBox( + width: computedSlotWidth, + child: (() { + switch (p) { + case PageItem.feed: + return BottomNavBarItem( + title: 'Feed', + imagePathActive: 'assets/img/icons/home-filled.png', + imagePathInactive: 'assets/img/icons/home-outlined.png', + onTap: () => widget.onSelectedPage(PageItem.feed), + isActive: widget.currentPage == PageItem.feed, + iconPaddingLeft: 0, + ); + case PageItem.events: + return BottomNavBarItem( + title: 'Events', + imagePathActive: 'assets/img/icons/calendar-filled.png', + imagePathInactive: 'assets/img/icons/calendar-outlined.png', + onTap: () => widget.onSelectedPage(PageItem.events), + isActive: widget.currentPage == PageItem.events, + iconPaddingLeft: 14, + ); + case PageItem.mensa: + return BottomNavBarItem( + title: 'Mensa', + imagePathActive: 'assets/img/icons/mensa-filled.png', + imagePathInactive: 'assets/img/icons/mensa-outlined.png', + onTap: () => widget.onSelectedPage(PageItem.mensa), + isActive: widget.currentPage == PageItem.mensa, + ); + case PageItem.navigation: + return BottomNavBarItem( + title: 'Navigation', + imagePathActive: 'assets/img/icons/map-filled.png', + imagePathInactive: 'assets/img/icons/map-outlined.png', + onTap: () => widget.onSelectedPage(PageItem.navigation), + isActive: widget.currentPage == PageItem.navigation, + ); + case PageItem.wallet: + return BottomNavBarItem( + title: 'Wallet', + imagePathActive: 'assets/img/icons/wallet-filled.png', + imagePathInactive: 'assets/img/icons/wallet-outlined.png', + onTap: () => widget.onSelectedPage(PageItem.wallet), + isActive: widget.currentPage == PageItem.wallet, + ); + case PageItem.more: + return BottomNavBarItem( + title: 'Mehr', + imagePathActive: 'assets/img/icons/more.png', + imagePathInactive: 'assets/img/icons/more.png', + onTap: () => widget.onSelectedPage(PageItem.more), + isActive: widget.currentPage == PageItem.more, + iconPaddingLeft: 5, + iconPaddingRight: 0, + ); + default: + return const SizedBox.shrink(); + } + })(), + ), + ], + ), + ), + ); + }), + ), + ); + }, ), ), ), diff --git a/lib/pages/home/widgets/bottom_nav_bar_item.dart b/lib/pages/home/widgets/bottom_nav_bar_item.dart index cea40b1..882544f 100644 --- a/lib/pages/home/widgets/bottom_nav_bar_item.dart +++ b/lib/pages/home/widgets/bottom_nav_bar_item.dart @@ -38,9 +38,9 @@ class BottomNavBarItem extends StatefulWidget { required this.imagePathActive, required this.imagePathInactive, required this.title, - this.iconVerticalPadding = 10, - this.iconPaddingLeft = 10, - this.iconPaddingRight = 10, + this.iconVerticalPadding = 8, + this.iconPaddingLeft = 0, + this.iconPaddingRight = 0, required this.onTap, this.isActive = false, }); @@ -64,7 +64,7 @@ class _BottomNavBarItemState extends State { return Padding( padding: EdgeInsets.only(left: widget.iconPaddingLeft, right: widget.iconPaddingRight), child: AnimatedPadding( - padding: widget.isActive ? const EdgeInsets.only(top: 2) : const EdgeInsets.only(top: 11), + padding: widget.isActive ? const EdgeInsets.only(top: 2) : const EdgeInsets.only(top: 6), duration: animationDuration, curve: animationCurve, child: Column( @@ -86,30 +86,29 @@ class _BottomNavBarItemState extends State { : Provider.of(context, listen: false).currentTheme == AppThemes.light ? Colors.black : const Color.fromRGBO(184, 186, 191, 1), - /* Provider.of(context, listen: false).currentTheme == AppThemes.light - ? widget.isActive - ? Provider.of(context).currentThemeData.colorScheme.secondary - : Colors.black - : widget.isActive - ? const Color.fromRGBO(255, 107, 1, 1) - : const Color.fromRGBO(184, 186, 191, 1), */ filterQuality: FilterQuality.high, ), ), ), - // Text - AnimatedPadding( - padding: widget.isActive ? EdgeInsets.zero : const EdgeInsets.only(top: 10), + // Text: use AnimatedSwitcher so inactive title does not reserve vertical + // space (prevents bottom overflow). This keeps a fade animation. + AnimatedSwitcher( duration: animationDuration, - curve: animationCurve, - child: AnimatedOpacity( - opacity: widget.isActive ? 1 : 0, - duration: animationDuration, - child: Text( - widget.title, - style: Provider.of(context).currentThemeData.textTheme.labelSmall, - ), - ), + switchInCurve: animationCurve, + switchOutCurve: animationCurve, + child: widget.isActive + ? FittedBox( + key: ValueKey('title-${widget.title}'), + fit: BoxFit.scaleDown, + alignment: Alignment.center, + child: Text( + widget.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Provider.of(context).currentThemeData.textTheme.labelSmall, + ), + ) + : const SizedBox.shrink(key: ValueKey('title-empty')), ), ], ), From 14cd8645a3c9f3ad560ae8ce0b3b8e9a4bcefaca Mon Sep 17 00:00:00 2001 From: Marc Gallert Date: Wed, 3 Sep 2025 18:05:06 +0200 Subject: [PATCH 2/6] Refactor Nav Bar to fix Padding andSizing issues --- lib/pages/home/widgets/bottom_nav_bar.dart | 212 +++++++++--------- .../home/widgets/bottom_nav_bar_item.dart | 62 ++--- 2 files changed, 136 insertions(+), 138 deletions(-) diff --git a/lib/pages/home/widgets/bottom_nav_bar.dart b/lib/pages/home/widgets/bottom_nav_bar.dart index 82c8402..6a7c240 100644 --- a/lib/pages/home/widgets/bottom_nav_bar.dart +++ b/lib/pages/home/widgets/bottom_nav_bar.dart @@ -40,8 +40,7 @@ class _BottomNavBarState extends State { // Minimal change approach: render all items in a fixed-width row but clip to // show only 5 slots. Animate a horizontal translation so one end icon slides // off-screen depending on the active page. - // Slightly reduce horizontal padding to avoid accidental right overflow - final horizontalPadding = 6.0; + final horizontalPadding = 10.0; // matches previous symmetric horizontal padding final visibleCount = 5; final items = [ PageItem.feed, @@ -64,18 +63,20 @@ class _BottomNavBarState extends State { // Compute height based on platform base and device bottom inset to avoid // overflow when system navigation/home bars reduce available height. final bottomInset = MediaQuery.of(context).padding.bottom; - // include bottom inset so the nav bar occupies enough height above system UI - // Reduce base heights slightly to make the navigation bar more compact. - final baseHeight = Platform.isIOS ? 76.0 : 86.0; - // Use the device bottom inset but avoid adding extra slack so the bar is - // noticeably shorter while still respecting safe areas. - final containerHeight = (baseHeight + bottomInset).clamp(baseHeight, baseHeight + 64.0); + // Keep the measured containerHeight equal to the base height so the + // container's bottom edge sits flush with the page bottom. Add the + // device bottom inset as inner padding so content doesn't overlap system UI. + final baseHeight = Platform.isIOS ? 88.0 : 98.0; + final containerHeight = baseHeight; return Container( height: containerHeight, - // remove the explicit bottom padding which reduced inner space and - // caused bottom overflow; keep only horizontal left padding - padding: Platform.isIOS ? const EdgeInsets.only(left: 5) : const EdgeInsets.only(left: 7), + // keep left padding but add bottom padding equal to the system inset so + // the visual content is above system UI while the container remains + // flush at the page bottom. + padding: Platform.isIOS + ? EdgeInsets.only(left: 5, bottom: bottomInset) + : EdgeInsets.only(left: 7, bottom: bottomInset), decoration: BoxDecoration( color: Provider.of(context).currentThemeData.cardColor, borderRadius: const BorderRadius.only( @@ -98,108 +99,100 @@ class _BottomNavBarState extends State { final navHeight = constraints.maxHeight; final effectiveContainerWidth = constraints.maxWidth; final computedSlotWidth = effectiveContainerWidth / visibleCount; - // computedTotalRowWidth and animatedLeft no longer needed because - // we render only the visible slice via AnimatedSwitcher. + + // Determine which slice of items to show (no off-screen items) + final startIndex = desiredShift; + final visibleItems = items.sublist(startIndex, startIndex + visibleCount); + + // Decide animation direction based on previous shift + final animateForward = desiredShift >= _prevShift; + // Update prevShift for next frame + _prevShift = desiredShift; return SizedBox( height: navHeight, width: effectiveContainerWidth, - child: SizedBox( - height: navHeight, - width: effectiveContainerWidth, - child: Builder(builder: (context) { - // Determine which slice of items to show (no off-screen items) - final startIndex = desiredShift; - final visibleItems = items.sublist(startIndex, startIndex + visibleCount); - - // Decide animation direction based on previous shift - final animateForward = desiredShift >= _prevShift; - // Update prevShift for next frame - _prevShift = desiredShift; - - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) { - final offsetAnimation = Tween( - begin: Offset(animateForward ? 1.0 : -1.0, 0), - end: Offset.zero, - ).animate(animation); - return SlideTransition(position: offsetAnimation, child: child); - }, - child: SizedBox( - width: effectiveContainerWidth, - child: Row( - key: ValueKey(desiredShift), - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - for (final p in visibleItems) - SizedBox( - width: computedSlotWidth, - child: (() { - switch (p) { - case PageItem.feed: - return BottomNavBarItem( - title: 'Feed', - imagePathActive: 'assets/img/icons/home-filled.png', - imagePathInactive: 'assets/img/icons/home-outlined.png', - onTap: () => widget.onSelectedPage(PageItem.feed), - isActive: widget.currentPage == PageItem.feed, - iconPaddingLeft: 0, - ); - case PageItem.events: - return BottomNavBarItem( - title: 'Events', - imagePathActive: 'assets/img/icons/calendar-filled.png', - imagePathInactive: 'assets/img/icons/calendar-outlined.png', - onTap: () => widget.onSelectedPage(PageItem.events), - isActive: widget.currentPage == PageItem.events, - iconPaddingLeft: 14, - ); - case PageItem.mensa: - return BottomNavBarItem( - title: 'Mensa', - imagePathActive: 'assets/img/icons/mensa-filled.png', - imagePathInactive: 'assets/img/icons/mensa-outlined.png', - onTap: () => widget.onSelectedPage(PageItem.mensa), - isActive: widget.currentPage == PageItem.mensa, - ); - case PageItem.navigation: - return BottomNavBarItem( - title: 'Navigation', - imagePathActive: 'assets/img/icons/map-filled.png', - imagePathInactive: 'assets/img/icons/map-outlined.png', - onTap: () => widget.onSelectedPage(PageItem.navigation), - isActive: widget.currentPage == PageItem.navigation, - ); - case PageItem.wallet: - return BottomNavBarItem( - title: 'Wallet', - imagePathActive: 'assets/img/icons/wallet-filled.png', - imagePathInactive: 'assets/img/icons/wallet-outlined.png', - onTap: () => widget.onSelectedPage(PageItem.wallet), - isActive: widget.currentPage == PageItem.wallet, - ); - case PageItem.more: - return BottomNavBarItem( - title: 'Mehr', - imagePathActive: 'assets/img/icons/more.png', - imagePathInactive: 'assets/img/icons/more.png', - onTap: () => widget.onSelectedPage(PageItem.more), - isActive: widget.currentPage == PageItem.more, - iconPaddingLeft: 5, - iconPaddingRight: 0, - ); - default: - return const SizedBox.shrink(); - } - })(), - ), - ], - ), - ), - ); - }), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + final offsetAnimation = Tween( + begin: Offset(animateForward ? 1.0 : -1.0, 0), + end: Offset.zero, + ).animate(animation); + return SlideTransition(position: offsetAnimation, child: child); + }, + child: SizedBox( + width: effectiveContainerWidth, + key: ValueKey(desiredShift), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + for (final p in visibleItems) + SizedBox( + width: computedSlotWidth, + child: (() { + switch (p) { + case PageItem.feed: + return BottomNavBarItem( + title: 'Feed', + imagePathActive: 'assets/img/icons/home-filled.png', + imagePathInactive: 'assets/img/icons/home-outlined.png', + onTap: () => widget.onSelectedPage(PageItem.feed), + isActive: widget.currentPage == PageItem.feed, + iconPaddingLeft: 0, + ); + case PageItem.events: + return BottomNavBarItem( + title: 'Events', + imagePathActive: 'assets/img/icons/calendar-filled.png', + imagePathInactive: 'assets/img/icons/calendar-outlined.png', + onTap: () => widget.onSelectedPage(PageItem.events), + isActive: widget.currentPage == PageItem.events, + iconPaddingLeft: 14, + ); + case PageItem.mensa: + return BottomNavBarItem( + title: 'Mensa', + imagePathActive: 'assets/img/icons/mensa-filled.png', + imagePathInactive: 'assets/img/icons/mensa-outlined.png', + onTap: () => widget.onSelectedPage(PageItem.mensa), + isActive: widget.currentPage == PageItem.mensa, + ); + case PageItem.navigation: + return BottomNavBarItem( + title: 'Navigation', + imagePathActive: 'assets/img/icons/map-filled.png', + imagePathInactive: 'assets/img/icons/map-outlined.png', + onTap: () => widget.onSelectedPage(PageItem.navigation), + isActive: widget.currentPage == PageItem.navigation, + ); + case PageItem.wallet: + return BottomNavBarItem( + title: 'Wallet', + imagePathActive: 'assets/img/icons/wallet-filled.png', + imagePathInactive: 'assets/img/icons/wallet-outlined.png', + onTap: () => widget.onSelectedPage(PageItem.wallet), + isActive: widget.currentPage == PageItem.wallet, + ); + case PageItem.more: + return BottomNavBarItem( + title: 'Mehr', + imagePathActive: 'assets/img/icons/more.png', + imagePathInactive: 'assets/img/icons/more.png', + onTap: () => widget.onSelectedPage(PageItem.more), + isActive: widget.currentPage == PageItem.more, + iconPaddingLeft: 5, + iconPaddingRight: 0, + ); + default: + return const SizedBox.shrink(); + } + })(), + ), + ], + ), + ), ), ); }, @@ -209,3 +202,4 @@ class _BottomNavBarState extends State { ); } } + diff --git a/lib/pages/home/widgets/bottom_nav_bar_item.dart b/lib/pages/home/widgets/bottom_nav_bar_item.dart index 882544f..b424fe5 100644 --- a/lib/pages/home/widgets/bottom_nav_bar_item.dart +++ b/lib/pages/home/widgets/bottom_nav_bar_item.dart @@ -68,45 +68,49 @@ class _BottomNavBarItemState extends State { duration: animationDuration, curve: animationCurve, child: Column( - mainAxisSize: MainAxisSize.min, + // Allow the column to expand to the available height from the + // parent slot; the icon area is wrapped in a Flexible with + // loose fit so it doesn't scale beyond its intrinsic size. This + // prevents vertical overflow while keeping the icon small. + mainAxisSize: MainAxisSize.max, children: [ - // Icon-button - CustomButton( - tapHandler: () => widget.onTap(), - child: Padding( - padding: EdgeInsets.only( - top: widget.iconVerticalPadding, - bottom: widget.iconVerticalPadding, - ), - child: Image.asset( - widget.isActive ? widget.imagePathActive : widget.imagePathInactive, - height: iconHeight, - color: widget.isActive - ? Provider.of(context).currentThemeData.colorScheme.secondary - : Provider.of(context, listen: false).currentTheme == AppThemes.light - ? Colors.black - : const Color.fromRGBO(184, 186, 191, 1), - filterQuality: FilterQuality.high, + // Icon-button wrapped so it can flex if parent is constrained + Flexible( + fit: FlexFit.loose, + child: CustomButton( + tapHandler: () => widget.onTap(), + child: Padding( + padding: EdgeInsets.only( + top: widget.iconVerticalPadding, + bottom: widget.iconVerticalPadding, + ), + child: Image.asset( + widget.isActive ? widget.imagePathActive : widget.imagePathInactive, + height: iconHeight, + color: widget.isActive + ? Provider.of(context).currentThemeData.colorScheme.secondary + : Provider.of(context, listen: false).currentTheme == AppThemes.light + ? Colors.black + : const Color.fromRGBO(184, 186, 191, 1), + filterQuality: FilterQuality.high, + ), ), ), ), - // Text: use AnimatedSwitcher so inactive title does not reserve vertical - // space (prevents bottom overflow). This keeps a fade animation. + + // Text: keep single line and avoid reserving vertical space when + // inactive. Use maxLines:1 to guarantee no wrapping. AnimatedSwitcher( duration: animationDuration, switchInCurve: animationCurve, switchOutCurve: animationCurve, child: widget.isActive - ? FittedBox( + ? Text( + widget.title, key: ValueKey('title-${widget.title}'), - fit: BoxFit.scaleDown, - alignment: Alignment.center, - child: Text( - widget.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Provider.of(context).currentThemeData.textTheme.labelSmall, - ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Provider.of(context).currentThemeData.textTheme.labelSmall, ) : const SizedBox.shrink(key: ValueKey('title-empty')), ), From f478c77bad22a4981955f7ae57d03c904bc53534 Mon Sep 17 00:00:00 2001 From: Marc Gallert Date: Wed, 3 Sep 2025 18:23:49 +0200 Subject: [PATCH 3/6] Fixed typos and item declarations --- lib/pages/home/widgets/bottom_nav_bar.dart | 21 +++++++++---------- .../home/widgets/bottom_nav_bar_item.dart | 10 ++++----- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/pages/home/widgets/bottom_nav_bar.dart b/lib/pages/home/widgets/bottom_nav_bar.dart index 6a7c240..e7ad2c0 100644 --- a/lib/pages/home/widgets/bottom_nav_bar.dart +++ b/lib/pages/home/widgets/bottom_nav_bar.dart @@ -35,14 +35,14 @@ class _BottomNavBarState extends State { // previous value so AnimatedSwitcher transition direction can be derived. // No-op here: _prevShift is updated in build to avoid extra setState. } + @override Widget build(BuildContext context) { // Minimal change approach: render all items in a fixed-width row but clip to // show only 5 slots. Animate a horizontal translation so one end icon slides // off-screen depending on the active page. - final horizontalPadding = 10.0; // matches previous symmetric horizontal padding - final visibleCount = 5; - final items = [ + final horizontalPadding = 10.0; // matches previous symmetric horizontal padding + const items = [ PageItem.feed, PageItem.events, PageItem.mensa, @@ -50,6 +50,7 @@ class _BottomNavBarState extends State { PageItem.wallet, PageItem.more, ]; + ]; final totalItems = items.length; final maxShift = (totalItems - visibleCount).clamp(0, totalItems); @@ -59,15 +60,14 @@ class _BottomNavBarState extends State { // requested behavior: when on first page show first 5, when on last show last 5. final desiredShift = (activeIndex - 2).clamp(0, maxShift).toInt(); - // Compute height based on platform base and device bottom inset to avoid // overflow when system navigation/home bars reduce available height. - final bottomInset = MediaQuery.of(context).padding.bottom; - // Keep the measured containerHeight equal to the base height so the - // container's bottom edge sits flush with the page bottom. Add the - // device bottom inset as inner padding so content doesn't overlap system UI. - final baseHeight = Platform.isIOS ? 88.0 : 98.0; - final containerHeight = baseHeight; + final bottomInset = MediaQuery.of(context).padding.bottom; + // Keep the measured containerHeight equal to the base height so the + // container's bottom edge sits flush with the page bottom. Add the + // device bottom inset as inner padding so content doesn't overlap system UI. + final baseHeight = Platform.isIOS ? 88.0 : 98.0; + final containerHeight = baseHeight; return Container( height: containerHeight, @@ -202,4 +202,3 @@ class _BottomNavBarState extends State { ); } } - diff --git a/lib/pages/home/widgets/bottom_nav_bar_item.dart b/lib/pages/home/widgets/bottom_nav_bar_item.dart index b424fe5..eded549 100644 --- a/lib/pages/home/widgets/bottom_nav_bar_item.dart +++ b/lib/pages/home/widgets/bottom_nav_bar_item.dart @@ -30,7 +30,7 @@ class BottomNavBarItem extends StatefulWidget { /// Callback that should be called whenever the button is tapped final VoidCallback onTap; - /// Wether the refered page is the currently displayed one + /// Whether the referred page is the currently displayed one final bool isActive; const BottomNavBarItem({ @@ -38,9 +38,9 @@ class BottomNavBarItem extends StatefulWidget { required this.imagePathActive, required this.imagePathInactive, required this.title, - this.iconVerticalPadding = 8, - this.iconPaddingLeft = 0, - this.iconPaddingRight = 0, + this.iconVerticalPadding = 8, + this.iconPaddingLeft = 0, + this.iconPaddingRight = 0, required this.onTap, this.isActive = false, }); @@ -64,7 +64,7 @@ class _BottomNavBarItemState extends State { return Padding( padding: EdgeInsets.only(left: widget.iconPaddingLeft, right: widget.iconPaddingRight), child: AnimatedPadding( - padding: widget.isActive ? const EdgeInsets.only(top: 2) : const EdgeInsets.only(top: 6), + padding: widget.isActive ? const EdgeInsets.only(top: 2) : const EdgeInsets.only(top: 6), duration: animationDuration, curve: animationCurve, child: Column( From 12f766c252076ed87a30e63cc3bd0c2771181102 Mon Sep 17 00:00:00 2001 From: Marc Gallert Date: Thu, 4 Sep 2025 10:24:44 +0200 Subject: [PATCH 4/6] Bug Fixes --- lib/pages/home/widgets/bottom_nav_bar.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pages/home/widgets/bottom_nav_bar.dart b/lib/pages/home/widgets/bottom_nav_bar.dart index e7ad2c0..686aafb 100644 --- a/lib/pages/home/widgets/bottom_nav_bar.dart +++ b/lib/pages/home/widgets/bottom_nav_bar.dart @@ -42,6 +42,7 @@ class _BottomNavBarState extends State { // show only 5 slots. Animate a horizontal translation so one end icon slides // off-screen depending on the active page. final horizontalPadding = 10.0; // matches previous symmetric horizontal padding + const visibleCount = 5; const items = [ PageItem.feed, PageItem.events, @@ -50,8 +51,6 @@ class _BottomNavBarState extends State { PageItem.wallet, PageItem.more, ]; - ]; - final totalItems = items.length; final maxShift = (totalItems - visibleCount).clamp(0, totalItems); final activeIndex = items.indexOf(widget.currentPage); From 948bcb17fa415f6f91d1e786b7e0d3324a3ef6bd Mon Sep 17 00:00:00 2001 From: Domai Date: Tue, 7 Oct 2025 09:29:34 +0200 Subject: [PATCH 5/6] fix: bottom padding was doubling the system UI bottom padding --- lib/pages/home/widgets/bottom_nav_bar.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/pages/home/widgets/bottom_nav_bar.dart b/lib/pages/home/widgets/bottom_nav_bar.dart index 686aafb..fdd2ce4 100644 --- a/lib/pages/home/widgets/bottom_nav_bar.dart +++ b/lib/pages/home/widgets/bottom_nav_bar.dart @@ -62,14 +62,11 @@ class _BottomNavBarState extends State { // Compute height based on platform base and device bottom inset to avoid // overflow when system navigation/home bars reduce available height. final bottomInset = MediaQuery.of(context).padding.bottom; - // Keep the measured containerHeight equal to the base height so the - // container's bottom edge sits flush with the page bottom. Add the - // device bottom inset as inner padding so content doesn't overlap system UI. - final baseHeight = Platform.isIOS ? 88.0 : 98.0; - final containerHeight = baseHeight; + // 66 = 26 Icon + 2*8 Vertical Padding + 14 Label Text + 12 Active Animation + const navbarHeight = 68; return Container( - height: containerHeight, + height: bottomInset + navbarHeight, // System UI + Campus App Navbar // keep left padding but add bottom padding equal to the system inset so // the visual content is above system UI while the container remains // flush at the page bottom. From 8d60d96fb5b578355d91ae300f1c2fffa9f80a6b Mon Sep 17 00:00:00 2001 From: Domai Date: Tue, 7 Oct 2025 09:32:32 +0200 Subject: [PATCH 6/6] apply dart fixes --- lib/pages/home/widgets/bottom_nav_bar.dart | 9 +++------ lib/pages/home/widgets/bottom_nav_bar_item.dart | 6 ------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/pages/home/widgets/bottom_nav_bar.dart b/lib/pages/home/widgets/bottom_nav_bar.dart index fdd2ce4..32c44f1 100644 --- a/lib/pages/home/widgets/bottom_nav_bar.dart +++ b/lib/pages/home/widgets/bottom_nav_bar.dart @@ -41,7 +41,7 @@ class _BottomNavBarState extends State { // Minimal change approach: render all items in a fixed-width row but clip to // show only 5 slots. Animate a horizontal translation so one end icon slides // off-screen depending on the active page. - final horizontalPadding = 10.0; // matches previous symmetric horizontal padding + const horizontalPadding = 10.0; // matches previous symmetric horizontal padding const visibleCount = 5; const items = [ PageItem.feed, @@ -57,7 +57,7 @@ class _BottomNavBarState extends State { // Aim to keep the active item roughly centered when possible; because // totalItems-visibleCount == 1 here, shift will be 0 or 1 which matches the // requested behavior: when on first page show first 5, when on last show last 5. - final desiredShift = (activeIndex - 2).clamp(0, maxShift).toInt(); + final desiredShift = (activeIndex - 2).clamp(0, maxShift); // Compute height based on platform base and device bottom inset to avoid // overflow when system navigation/home bars reduce available height. @@ -88,7 +88,7 @@ class _BottomNavBarState extends State { ], ), child: Padding( - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + padding: const EdgeInsets.symmetric(horizontal: horizontalPadding), child: ClipRect( child: LayoutBuilder( builder: (context, constraints) { @@ -122,7 +122,6 @@ class _BottomNavBarState extends State { key: ValueKey(desiredShift), child: Row( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, children: [ for (final p in visibleItems) SizedBox( @@ -136,7 +135,6 @@ class _BottomNavBarState extends State { imagePathInactive: 'assets/img/icons/home-outlined.png', onTap: () => widget.onSelectedPage(PageItem.feed), isActive: widget.currentPage == PageItem.feed, - iconPaddingLeft: 0, ); case PageItem.events: return BottomNavBarItem( @@ -179,7 +177,6 @@ class _BottomNavBarState extends State { onTap: () => widget.onSelectedPage(PageItem.more), isActive: widget.currentPage == PageItem.more, iconPaddingLeft: 5, - iconPaddingRight: 0, ); default: return const SizedBox.shrink(); diff --git a/lib/pages/home/widgets/bottom_nav_bar_item.dart b/lib/pages/home/widgets/bottom_nav_bar_item.dart index eded549..e31a759 100644 --- a/lib/pages/home/widgets/bottom_nav_bar_item.dart +++ b/lib/pages/home/widgets/bottom_nav_bar_item.dart @@ -68,15 +68,9 @@ class _BottomNavBarItemState extends State { duration: animationDuration, curve: animationCurve, child: Column( - // Allow the column to expand to the available height from the - // parent slot; the icon area is wrapped in a Flexible with - // loose fit so it doesn't scale beyond its intrinsic size. This - // prevents vertical overflow while keeping the icon small. - mainAxisSize: MainAxisSize.max, children: [ // Icon-button wrapped so it can flex if parent is constrained Flexible( - fit: FlexFit.loose, child: CustomButton( tapHandler: () => widget.onTap(), child: Padding(