diff --git a/modules/ensemble/lib/framework/view/bottom_nav_controller.dart b/modules/ensemble/lib/framework/view/bottom_nav_controller.dart new file mode 100644 index 000000000..f5da701ad --- /dev/null +++ b/modules/ensemble/lib/framework/view/bottom_nav_controller.dart @@ -0,0 +1,77 @@ +import 'package:ensemble/framework/menu.dart'; +import 'package:flutter/material.dart'; + +/// Singleton controller that stores and provides access to built bottom navigation widgets +class GlobalBottomNavController extends ChangeNotifier { + static GlobalBottomNavController? _instance; + + // Private constructor + GlobalBottomNavController._(); + + // Singleton instance getter + static GlobalBottomNavController get instance { + _instance ??= GlobalBottomNavController._(); + return _instance!; + } + + // Stored widgets + Widget? _storedBottomNavWidget; + Widget? _storedFloatingActionButton; + FloatingActionButtonLocation? _storedFloatingActionButtonLocation; + + // Getters + Widget? get bottomNavWidget => _storedBottomNavWidget; + Widget? get floatingActionButton => _storedFloatingActionButton; + FloatingActionButtonLocation? get floatingActionButtonLocation => + _storedFloatingActionButtonLocation; + + set bottomNavWidget(Widget? widget) { + _storedBottomNavWidget = widget; + notifyListeners(); + } + + set floatingActionButton(Widget? widget) { + _storedFloatingActionButton = widget; + notifyListeners(); + } + + set floatingLocation(FloatingActionButtonLocation? widget) { + _storedFloatingActionButtonLocation = widget; + notifyListeners(); + } + + + /// Unregister widgets when PageGroup is disposed + void unregisterBottomNavWidgets() { + _storedBottomNavWidget = null; + _storedFloatingActionButton = null; + _storedFloatingActionButtonLocation = null; + + notifyListeners(); + } + + /// Get bottom nav widget + Widget? getBottomNavWidget() { + return _storedBottomNavWidget != null + ? Container(child: _storedBottomNavWidget) + : null; + } + + /// Get floating action button + Widget? getFloatingActionButton() { + return _storedFloatingActionButton != null + ? Container(child: _storedFloatingActionButton) + : null; + } + + @override + void dispose() { + super.dispose(); + } +} + +/// Extension to make it easier to access the controller +extension GlobalBottomNavControllerExtension on BuildContext { + GlobalBottomNavController get globalBottomNav => + GlobalBottomNavController.instance; +} \ No newline at end of file diff --git a/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart b/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart index 70e59c8cf..fa5bd14f3 100644 --- a/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart +++ b/modules/ensemble/lib/framework/view/bottom_nav_page_group.dart @@ -15,6 +15,7 @@ import 'package:ensemble/util/utils.dart'; import 'package:ensemble/framework/widget/icon.dart' as ensemble; import 'package:ensemble/widget/helpers/controllers.dart'; import 'package:flutter/material.dart'; +import 'package:ensemble/framework/view/bottom_nav_controller.dart'; class BottomNavBarItem { BottomNavBarItem({ @@ -27,6 +28,7 @@ class BottomNavBarItem { this.switchScreen = true, this.onTap, this.onTapHaptic, + this.page, }); Widget icon; @@ -38,6 +40,7 @@ class BottomNavBarItem { bool? switchScreen; EnsembleAction? onTap; String? onTapHaptic; + String? page; } enum FloatingAlignment { @@ -152,6 +155,8 @@ class _BottomNavPageGroupState extends State @override void dispose() { + // Unregister the bottom nav widgets when the PageGroup is disposed + GlobalBottomNavController.instance.unregisterBottomNavWidgets(); if (widget.menu.reloadView == false) { controller.dispose(); } @@ -221,18 +226,27 @@ class _BottomNavPageGroupState extends State Utils.getColor(widget.menu.runtimeStyles?['notchColor']) ?? Theme.of(context).scaffoldBackgroundColor; + // Build the widgets + Widget? bottomNavBar = _buildBottomNavBar(); + Widget? floatingButton = _buildFloatingButton(); + FloatingActionButtonLocation? floatingLocation = + floatingAlignment == FloatingAlignment.none + ? null + : floatingAlignment.location; + WidgetsBinding.instance.addPostFrameCallback((_) { + GlobalBottomNavController.instance.bottomNavWidget = bottomNavBar; + GlobalBottomNavController.instance.floatingActionButton = floatingButton; + GlobalBottomNavController.instance.floatingLocation = floatingLocation; + }); return PageGroupWidgetWrapper( reloadView: widget.menu.reloadView, scopeManager: widget.scopeManager, child: Scaffold( resizeToAvoidBottomInset: true, backgroundColor: notchColor, - bottomNavigationBar: _buildBottomNavBar(), - floatingActionButtonLocation: - floatingAlignment == FloatingAlignment.none - ? null - : floatingAlignment.location, - floatingActionButton: _buildFloatingButton(), + bottomNavigationBar: bottomNavBar, + floatingActionButtonLocation: floatingLocation, + floatingActionButton: floatingButton, body: widget.menu.reloadView == true ? ListenableBuilder( listenable: viewGroupNotifier, @@ -347,7 +361,16 @@ class _BottomNavPageGroupState extends State if (widget.menu.reloadView == true) { viewGroupNotifier.updatePage(index); } else { - PageGroupWidget.getPageController(context)!.jumpToPage(index); + // Continue the old flow if PageController is available + if (PageGroupWidget.getPageController(context) != null) { + PageGroupWidget.getPageController(context)!.jumpToPage(index); + } else if (GlobalBottomNavController.instance.bottomNavWidget != + null) { + // This to make sure that if nav buttons are used outside the page group, they would still function + ScreenController().navigateToScreen(context, + screenName: navItems[index].page, + pageArgs: viewGroupNotifier.payload); + } viewGroupNotifier.updatePage(index); } diff --git a/modules/ensemble/lib/framework/view/page.dart b/modules/ensemble/lib/framework/view/page.dart index 84dba503d..15920c35a 100644 --- a/modules/ensemble/lib/framework/view/page.dart +++ b/modules/ensemble/lib/framework/view/page.dart @@ -1,5 +1,5 @@ import 'dart:developer'; - +import 'bottom_nav_controller.dart'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/action.dart'; import 'package:ensemble/ensemble_app.dart'; @@ -468,6 +468,9 @@ class PageState extends State EnsembleThemeManager().configureStyles(_scopeManager.dataContext, widget._pageModel.menu!, widget._pageModel.menu!); } + final globalBottomNav = GlobalBottomNavController.instance; + Widget? _floatingActionButton; + FloatingActionButtonLocation? _floatingActionButtonLocation; // build the navigation menu (bottom nav bar or drawer). Note that menu is not applicable on modal pages if (widget._pageModel.menu != null && widget._pageModel.screenOptions?.pageType != PageType.modal) { @@ -486,6 +489,13 @@ class PageState extends State // sidebar navBar will be rendered as part of the body } + if (globalBottomNav.bottomNavWidget != null && + widget._pageModel.runtimeStyles?['showMenu'] == true) { + _bottomNavBar = globalBottomNav.bottomNavWidget; + _floatingActionButton = globalBottomNav.floatingActionButton; + _floatingActionButtonLocation = + globalBottomNav.floatingActionButtonLocation; + } LinearGradient? backgroundGradient = Utils.getBackgroundGradient( widget._pageModel.runtimeStyles?['backgroundGradient']); Color? backgroundColor = Utils.getColor(_scopeManager.dataContext @@ -553,12 +563,13 @@ class PageState extends State bottomNavigationBar: _bottomNavBar, drawer: _drawer, endDrawer: _endDrawer, - floatingActionButton: closeModalButton, - floatingActionButtonLocation: - widget._pageModel.runtimeStyles?['navigationIconPosition'] == + floatingActionButton: _floatingActionButton ?? closeModalButton, + floatingActionButtonLocation: _floatingActionButtonLocation ?? + ( + widget._pageModel.runtimeStyles?['navigationIconPosition'] == 'start' ? FloatingActionButtonLocation.startTop - : FloatingActionButtonLocation.endTop), + : FloatingActionButtonLocation.endTop)), ), ); DevMode.pageDataContext = _scopeManager.dataContext; @@ -588,7 +599,7 @@ class PageState extends State } return rtn; } - + /// determine if we should wraps the body in a SafeArea or not bool useSafeArea() { bool? useSafeArea = @@ -904,26 +915,26 @@ class AnimatedAppBar extends StatefulWidget { final duration; AnimatedAppBar( {Key? key, - this.automaticallyImplyLeading, - this.leadingWidget, - this.titleWidget, - this.centerTitle, - this.backgroundColor, - this.surfaceTintColor, - this.foregroundColor, - this.elevation, - this.shadowColor, - this.titleBarHeight, - this.backgroundWidget, - this.animated, - this.floating, - this.pinned, - this.collapsedBarHeight, - this.expandedBarHeight, - required this.scrollController, - this.curve, - this.animationType, - this.duration}) + this.automaticallyImplyLeading, + this.leadingWidget, + this.titleWidget, + this.centerTitle, + this.backgroundColor, + this.surfaceTintColor, + this.foregroundColor, + this.elevation, + this.shadowColor, + this.titleBarHeight, + this.backgroundWidget, + this.animated, + this.floating, + this.pinned, + this.collapsedBarHeight, + this.expandedBarHeight, + required this.scrollController, + this.curve, + this.animationType, + this.duration}) : super(key: key); @override @@ -932,7 +943,7 @@ class AnimatedAppBar extends StatefulWidget { class _AnimatedAppBarState extends State with WidgetsBindingObserver{ bool isCollapsed = false; - + @override void initState() { super.initState(); @@ -997,21 +1008,21 @@ class _AnimatedAppBarState extends State with WidgetsBindingObse centerTitle: widget.centerTitle, title: widget.animated ? switch (widget.animationType) { - AnimationType.fade => AnimatedOpacity( - opacity: isCollapsed ? 1.0 : 0.0, - duration: Duration(milliseconds: widget.duration ?? 300), - curve: widget.curve ?? Curves.easeIn, - child: widget.titleWidget, - ), - AnimationType.drop => AnimatedSlide( - offset: isCollapsed ? Offset(0, 0) : Offset(0, -2), - duration: Duration(milliseconds: widget.duration ?? 300), - curve: widget.curve ?? Curves.easeIn, - child: widget.titleWidget, - ), - _ => widget.titleWidget, - } - : widget.titleWidget, + AnimationType.fade => AnimatedOpacity( + opacity: isCollapsed ? 1.0 : 0.0, + duration: Duration(milliseconds: widget.duration ?? 300), + curve: widget.curve ?? Curves.easeIn, + child: widget.titleWidget, + ), + AnimationType.drop => AnimatedSlide( + offset: isCollapsed ? Offset(0, 0) : Offset(0, -2), + duration: Duration(milliseconds: widget.duration ?? 300), + curve: widget.curve ?? Curves.easeIn, + child: widget.titleWidget, + ), + _ => widget.titleWidget, + } + : widget.titleWidget, elevation: widget.elevation, backgroundColor: widget.backgroundColor, flexibleSpace: wrapsInFlexible(widget.backgroundWidget),