First of all, thank you very much for your project, which has been of great help to me. But when I use the footer feature of SideMenu, I am unable to build SideMenuItems in the footer. Even if I use SideMenuItemWithGlobal, because there is only one item list in Global, I cannot achieve page redirection and other features. Therefore, based on your original code, I have added a footerMenuItems list to maintain the SideMenu at the bottom. Due to my limited abilities, I did not incorporate it into your original code. If you find this feature useful, you may consider adding it to the project. Thanks.
import 'package:easy_sidemenu/easy_sidemenu.dart';
// ignore: implementation_imports
import 'package:easy_sidemenu/src/global/global.dart';
import 'package:flutter/material.dart';
import 'package:badges/badges.dart' as bdg;
class FooterMenu extends StatefulWidget {
/// List of [SideMenuItem] on [FooterMenu]
final List<SideMenuItem> footerItems;
/// divider Widget
final Widget? divider;
const FooterMenu({
super.key,
required this.footerItems,
this.divider,
}) : super();
@override
State<FooterMenu> createState() => _FooterMenuState();
}
class _FooterMenuState extends State<FooterMenu> {
/// SideMenu's global
late final Global? _global;
@override
void initState() {
super.initState();
/// get SideMenu's global
_global = context.findAncestorWidgetOfExactType<SideMenu>()?.global;
assert(_global != null, 'get global exception');
}
@override
void dispose() {
/// release
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
widget.divider ??
const SizedBox.shrink(),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...widget.footerItems.map((item) {
if (item.builder == null) {
return FooterItem(
global: _global!,
item: item,
footerItems: widget.footerItems,
);
} else {
return ValueListenableBuilder(
valueListenable: _global!.displayModeState,
builder: (context, value, child) {
return item.builder!(context, value);
},
);
}
}).toList()
],
),
),
),
],
);
}
}
class FooterItem extends StatefulWidget {
/// Global
final Global global;
/// SideMenuItem
final SideMenuItem item;
/// List of [SideMenuItem] on [FooterMenu]
final List<SideMenuItem> footerItems;
const FooterItem({
super.key,
required this.global,
required this.item,
required this.footerItems,
});
@override
State<FooterItem> createState() => _FooterItemState();
}
class _FooterItemState extends State<FooterItem> {
/// use to set backgroundColor
bool isHovered = false;
bool isDisposed = false;
late int currentPage;
@override
void initState() {
super.initState();
currentPage = widget.global.controller.currentPage;
_nonNullableWrap(WidgetsBinding.instance)!
.addPostFrameCallback((timeStamp) {
// Set initialPage, only if the widget is still mounted
if (mounted) {
currentPage = widget.global.controller.currentPage;
}
if (!isDisposed) {
// Set controller SideMenuItem page controller callback
widget.global.controller.addListener(_handleChange);
}
});
widget.global.itemsUpdate.add(update);
}
@override
void dispose() {
isDisposed = true;
widget.global.controller.removeListener(_handleChange);
super.dispose();
}
/// This allows a value of type T or T?
/// to be treated as a value of type T?.
///
/// We use this so that APIs that have become
/// non-nullable can still be used with `!` and `?`
/// to support older versions of the API as well.
/// https://docs.flutter.dev/development/tools/sdk/release-notes/release-notes-3.0.0#your-code
T? _nonNullableWrap<T>(T? value) => value;
void update() {
if (mounted) {
// Trigger a build only if the widget is still mounted
setState(() {});
}
}
void _handleChange(int page) {
safeSetState(() {
currentPage = page;
});
}
/// Ensure that safeSetState only calls setState when the widget is still mounted.
///
/// When adding changes to this library in future, use this function instead of
/// if (mounted) condition on setState at every place
void safeSetState(VoidCallback fn) {
if (mounted) {
setState(fn);
}
}
/// use to judge isSelected
bool get _isSelected {
if (widget.footerItems.indexOf(widget.item) + widget.global.items.length ==
currentPage) {
return true;
} else {
return false;
}
}
/// Set background color of [FooterMenu]
Color _setColor() {
if (_isSelected) {
if (isHovered) {
return widget.global.style.selectedHoverColor ??
widget.global.style.selectedColor ??
Theme.of(context).highlightColor;
} else {
return widget.global.style.selectedColor ??
Theme.of(context).highlightColor;
}
} else if (isHovered) {
return widget.global.style.hoverColor ?? Colors.transparent;
} else {
return Colors.transparent;
}
}
/// Set icon for of [FooterMenuItem]
Widget _generateIcon(SideMenuItem item) {
if (item.icon == null) return item.iconWidget ?? const SizedBox();
Icon icon = Icon(
item.icon!.icon,
color: _isSelected
? widget.global.style.selectedIconColor ?? Colors.black
: widget.global.style.unselectedIconColor ?? Colors.black54,
size: widget.global.style.iconSize ?? 24,
);
if (item.badgeContent == null) {
return icon;
} else {
return bdg.Badge(
badgeContent: item.badgeContent!,
badgeStyle: bdg.BadgeStyle(
badgeColor: item.badgeColor ?? Colors.red,
),
position: bdg.BadgePosition.topEnd(top: -13, end: -7),
child: icon,
);
}
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => widget.item.onTap?.call(
widget.global.items.length + widget.footerItems.indexOf(widget.item),
widget.global.controller,
),
onHover: (value) {
safeSetState(() {
isHovered = value;
});
},
highlightColor: Colors.transparent,
focusColor: Colors.transparent,
hoverColor: Colors.transparent,
splashColor: Colors.transparent,
child: Padding(
padding: widget.global.style.itemOuterPadding,
child: Container(
height: widget.global.style.itemHeight,
width: double.infinity,
decoration: BoxDecoration(
color: _setColor(),
borderRadius: widget.global.style.itemBorderRadius,
),
child: ValueListenableBuilder(
valueListenable: widget.global.displayModeState,
builder: (context, value, child) {
return Tooltip(
message: (value == SideMenuDisplayMode.compact &&
widget.global.style.showTooltip)
? widget.item.tooltipContent ?? widget.item.title ?? ""
: "",
child: Padding(
padding: EdgeInsets.symmetric(
vertical: value == SideMenuDisplayMode.compact ? 0 : 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: widget.global.style.itemInnerSpacing,
),
_generateIcon(widget.item),
SizedBox(
width: widget.global.style.itemInnerSpacing,
),
if (value == SideMenuDisplayMode.open) ...[
Expanded(
child: FittedBox(
alignment:
Directionality.of(context) == TextDirection.ltr
? Alignment.centerLeft
: Alignment.centerRight,
fit: BoxFit.scaleDown,
child: Text(
widget.item.title ?? '',
style: _isSelected
? const TextStyle(
fontSize: 17, color: Colors.black)
.merge(widget
.global.style.selectedTitleTextStyle)
: const TextStyle(
fontSize: 17, color: Colors.black54)
.merge(widget.global.style
.unselectedTitleTextStyle),
),
),
),
if (widget.item.trailing != null &&
widget.global.showTrailing) ...[
widget.item.trailing!,
SizedBox(
width: widget.global.style.itemInnerSpacing,
),
],
],
],
),
),
);
},
),
),
),
);
}
}
First of all, thank you very much for your project, which has been of great help to me. But when I use the footer feature of SideMenu, I am unable to build SideMenuItems in the footer. Even if I use SideMenuItemWithGlobal, because there is only one item list in Global, I cannot achieve page redirection and other features. Therefore, based on your original code, I have added a footerMenuItems list to maintain the SideMenu at the bottom. Due to my limited abilities, I did not incorporate it into your original code. If you find this feature useful, you may consider adding it to the project. Thanks.
Using it: