Jamalianpour / easy_sidemenu

An easy to use side menu (navigation rail) in flutter and can used for navigations
https://pub.dev/packages/easy_sidemenu
MIT License
145 stars 70 forks source link

Add FooterMenu feature #75

Open ZhongGuanbin opened 11 months ago

ZhongGuanbin commented 11 months ago

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,
                          ),
                        ],
                      ],
                    ],
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

Using it:


footer: Builder(
                      builder: (BuildContext context) {
                        return ConstrainedBox(
                          constraints: const BoxConstraints(
                            maxHeight: 116,
                          ),
                          child: Container(
                            color: context
                                .findAncestorWidgetOfExactType<SideMenu>()!
                                .style
                                ?.backgroundColor,
                            child: FooterMenu(footerItems: sideMenuFooterItems, divider: const Divider(
                              endIndent: 8,
                              indent: 8,
                            ),),
                          ),
                        );
                      },
                    ),
aditya113141 commented 11 months ago

Hey, can you show video demo of your solution ?

ZhongGuanbin commented 11 months ago

来信获悉,感谢您的支持!耑此奉复 钟冠彬This is an automatic reply, confirming that your e-mail was received.Thank you