Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FooterMenu feature #75

Open
ZhongGuanbin opened this issue Nov 24, 2023 · 2 comments
Open

Add FooterMenu feature #75

ZhongGuanbin opened this issue Nov 24, 2023 · 2 comments

Comments

@ZhongGuanbin
Copy link

ZhongGuanbin commented Nov 24, 2023

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
Copy link
Contributor

Hey, can you show video demo of your solution ?

@ZhongGuanbin
Copy link
Author

ZhongGuanbin commented Dec 9, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants