flutter_smenus 2.0.0 copy "flutter_smenus: ^2.0.0" to clipboard
flutter_smenus: ^2.0.0 copied to clipboard

Flutter package for all sorts of menus from dropdowns to sliding to resizable to navigation menus

example/example.dart

import 'package:flutter/material.dart';

import 'package:flutter_smenus/dropdown.dart';
import 'package:flutter_smenus/menu.dart';
import 'package:flutter_smenus/menu_item.dart';
import 'package:flutter_smenus/base.dart';

/// Note: This example also implements a custom indicator for the clickable
/// menu buttons on the left, which extend the [SMenuItem] class. This is
/// unrelated to the package, but it is implemented in this example.

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter SMenus',
      theme: ThemeData(),
      home: const MyHomePage(title: 'Flutter SMenus'),
      debugShowCheckedModeBanner: false,
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // A controller for each menu
  SMenuController detailsMenuController = SMenuController();
  SMenuController leftMenuController = SMenuController();
  SMenuController consoleMenuController = SMenuController();
  // Data for details menu (current chosen item in ListView)
  Map<String, dynamic> chosenFile = data[1];
  // the current selected menu button (custom class found below this class called SMenuItemButton)
  int selectedIndex = 0;
  // Keys for the left hand side menu items and allows us to grab their heights, see below for more info.
  List<GlobalKey> keys = List.generate(4, (index) => GlobalKey());
  List<double> heights = [];

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() {
        heights = keys
            .map((key) => key.currentContext!.size!.height)
            .toList()
            .reversed
            .toList();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    // a variable to hold the style for dropdown menu items that are in the middle,
    // this was repetitive so it was made into a variable
    const midDropdownButtonStyle = SMenuItemStyle(
        borderRadius: BorderRadius.zero, alignment: MainAxisAlignment.center);
    // Build app
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        actions: const [
          Padding(
            padding: EdgeInsets.only(right: 12.0),
            child: Center(
              child: SizedBox.square(
                dimension: 32,
                // Settings dropdown menu that is on the right of the app bar
                child: SDropdownMenuCascade(
                  position: SDropdownMenuPosition.bottomLeft,
                  icon: Icon(
                    Icons.settings,
                    size: 22,
                  ),
                  items: [
                    // The top and bottom menu items have special
                    // border radius to create a rounded rectangle look of
                    // the dropdown. This is because the clickable item
                    // has its own background.
                    SMenuItem.clickable(
                      value: 1,
                      title: Text('First Option (1)'),
                      style: SMenuItemStyle(
                        borderRadius: BorderRadius.only(
                          topLeft: Radius.circular(15),
                          topRight: Radius.circular(15),
                        ),
                      ),
                    ),
                    SMenuItem.clickable(
                      value: 2,
                      title: Text('Option 2'),
                      style: midDropdownButtonStyle,
                    ),
                    SMenuItem.clickable(
                      value: 3,
                      title: Text('Option 3'),
                      style: midDropdownButtonStyle,
                    ),
                    SMenuItem.clickable(
                      value: 4,
                      title: Text('Option 4'),
                      style: midDropdownButtonStyle,
                    ),
                    SMenuItem.clickable(
                      value: 5,
                      title: Text('Last Option (5)'),
                      style: SMenuItemStyle(
                        borderRadius: BorderRadius.only(
                          bottomLeft: Radius.circular(15),
                          bottomRight: Radius.circular(15),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          )
        ],
      ),

      // Persistent main menu
      body: SResizableMenu(
        // specify size so buttons fit
        closedSize: 60,
        controller: leftMenuController,
        resizable:
            false, // Remove resize bar, user shouldn't be able to control size
        position: SMenuPosition.left,
        style: SMenuStyle(
            border: Border.all(color: Colors.black12, width: 1),
            borderRadius:
                const BorderRadius.horizontal(right: Radius.circular(15))),
        // add a button to toggle the menu
        header: TextButton(
            onPressed: () {
              leftMenuController.toggle();
              setState(() {});
            },
            // Icon changes based on the state, could also use a listener on
            // the controller state instead of doing this
            child: Icon((leftMenuController.state.value == SMenuState.closed ||
                    leftMenuController.state.value == SMenuState.closing)
                ? Icons.menu
                : Icons.menu_open_outlined)),
        footer: const Padding(
          padding: EdgeInsets.symmetric(horizontal: 25),
          child: Text(
            'This is a basic footer',
            textAlign: TextAlign.center,
            maxLines: 1,
          ),
        ),
        builder: (context, items) {
          // Use the height of all widgets that are above it, and half the current
          // button widget's height, to calculate the offset from the top of the menu
          // (under the header) that the animated container that is the indicator
          // should move.

          double offset = 0.0;
          int val = -1;
          // calc offset
          for (int i = 0; i < heights.length; i++) {
            if (items[i] is SMenuItemButton) {
              val++;
            }

            if (val == selectedIndex) {
              offset += heights[i] / 2;

              break;
            }
            if (i == 0) {
              offset += 20 / 2;
            }
            offset += heights[i];
          }

          // Create a stack so that the animated container can lie on top of the buttons
          return Stack(
            children: [
              SingleChildScrollView(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: items,
                ),
              ),
              // This animated container is the actual indicator that shows what
              // button is selected. It animates between different button for a
              // smooth transition effect.
              AnimatedContainer(
                  duration: const Duration(milliseconds: 250),
                  curve: Curves.easeInOutCirc,
                  // This top margin is the offset calculated earlier
                  margin: EdgeInsets.only(top: offset, left: 2),
                  height: 25,
                  width: 5.0,
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(5),
                      color: Theme.of(context).colorScheme.onPrimary)),
            ],
          );
        },
        items: [
          // Creates our custom class SMenuItemButton which enables us to show
          // that a button is selected. The key is given so that we can have a way
          // to measure the height of the widget.
          SMenuItemButton(
            key: keys[0],
            text: 'Home',
            isSelected: selectedIndex == 0,
            icon: Icons.home,
            onTap: () {
              setState(() {
                selectedIndex = 0;
              });
            },
          ),
          SMenuItemButton(
            key: keys[1],
            text: 'A Page',
            isSelected: selectedIndex == 1,
            icon: Icons.file_open,
            onTap: () {
              setState(() {
                selectedIndex = 1;
              });
            },
          ),
          SMenuItemButton(
            key: keys[2],
            text: 'Another Page',
            isSelected: selectedIndex == 2,
            icon: Icons.document_scanner,
            onTap: () {
              setState(() {
                selectedIndex = 2;
              });
            },
          ),
          // Creates a literal label. Not clickable.
          SMenuItem.label(
            key: keys[3],
            title: const Padding(
              padding: EdgeInsets.symmetric(horizontal: 15),
              child: Text(
                'This is a label',
                textAlign: TextAlign.center,
                maxLines: 1,
              ),
            ),
            style: midDropdownButtonStyle,
          ),
        ],

        // Console Menu
        body: SResizableMenu(
          controller: consoleMenuController,
          position: SMenuPosition.bottom,
          items: [
            // Creates a rounded rectangle container to act as some filler
            SMenuItem(
              builder: (context, style, child) {
                return Container(
                  height: MediaQuery.of(context).size.height / 3.5,
                  width: MediaQuery.of(context).size.width,
                  decoration: BoxDecoration(
                      color: Colors.black12,
                      borderRadius: BorderRadius.circular(15)),
                );
              },
            )
          ],
          // Create the header, with text on the left, and a dropdown menu along
          // with the menu toggle button on the right
          header: SizedBox(
            height: 50,
            child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  const Flexible(
                      child: Text(
                    'Command Bar',
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  )),
                  Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const SizedBox.square(
                        dimension: 40,
                        child: Center(
                          child: SDropdownMenuMorph(
                            height: 150,
                            position: SDropdownMenuPosition.topLeft,
                            icon: Icon(Icons.menu),
                            items: [
                              // The top and bottom menu items have special
                              // border radius to create a rounded rectangle look of
                              // the dropdown. This is because the clickable item
                              // has its own background.
                              SMenuItem.clickable(
                                value: 1,
                                title: Text('First Option (1)'),
                                style: SMenuItemStyle(
                                  borderRadius: BorderRadius.only(
                                    topLeft: Radius.circular(15),
                                    topRight: Radius.circular(15),
                                  ),
                                ),
                              ),
                              SMenuItem.clickable(
                                value: 2,
                                title: Text('Option 2'),
                                style: midDropdownButtonStyle,
                              ),
                              SMenuItem.clickable(
                                value: 3,
                                title: Text('Option 3'),
                                style: midDropdownButtonStyle,
                              ),
                              SMenuItem.clickable(
                                value: 4,
                                title: Text('Option 4'),
                                style: midDropdownButtonStyle,
                              ),
                              SMenuItem.clickable(
                                value: 5,
                                title: Text('Last Option (5)'),
                                style: SMenuItemStyle(
                                  borderRadius: BorderRadius.only(
                                    bottomLeft: Radius.circular(15),
                                    bottomRight: Radius.circular(15),
                                  ),
                                ),
                              ),
                            ],
                          ),
                        ),
                      ),
                      // Open/close the menu
                      TextButton(
                          onPressed: () {
                            consoleMenuController.toggle();
                          },
                          child: const Icon(Icons.keyboard_command_key)),
                    ],
                  ),
                ]),
          ),

          // Details Menu
          body: SSlideMenu(
            closedSize: 60,
            openSize: 400,
            position: SMenuPosition.right,
            controller: detailsMenuController,
            isBodyMovable: false,
            items: [
              // Create a menu item for each item in the currently chosen map
              // These are the details shown when you click an item in the ListView
              for (String key in chosenFile.keys)
                SMenuItem(
                  child: Column(
                    children: [
                      Row(
                        children: [
                          Padding(
                            padding: const EdgeInsets.only(left: 8.0),
                            child: Text(
                              '$key:',
                              style: const TextStyle(fontSize: 16),
                            ),
                          ),
                        ],
                      ),
                      Text(chosenFile[key]),
                    ],
                  ),
                ),
            ],
            // Build the list view for the data. This is the actual "app"
            body: Container(
              color: Colors.white,
              child: Center(
                  child: ListView(
                children: [
                  for (Map<String, dynamic> item in data)
                    ListTile(
                      leading: Text(item['name']),
                      title: Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Text(item['location']),
                        ],
                      ),
                      trailing: Text(item['size']),
                      onTap: () {
                        setState(() {
                          chosenFile = item;
                        });
                        detailsMenuController.toggle();
                      },
                    )
                ],
              )),
            ),
          ),
        ),
      ),
    );
  }
}

/// This is a custom class that extends the [SMenuItem]. All it does is create
/// a custom widget using the child argument. This class exists to wrap certain
/// variables, such as isSelected. This allows us to create a TextButton in place
/// of the regular menu item. We want the button to be a certain
/// color when selected. This is a rudimentary implementation of a Menu Button,
/// which means it is not fully fledged out.
///
/// Used in the left hand side menu in this example.
class SMenuItemButton<T> extends SMenuItem {
  const SMenuItemButton({
    super.key,
    this.onTap,
    super.style = const SMenuItemStyle(),
    required this.icon,
    this.selectedTextColor,
    this.selectedIconColor,
    this.textColor,
    this.iconColor,
    this.text,
    this.isSelected = false,
    this.onHover,
    this.onLongPress,
  });
  final IconData icon;
  final Color? selectedTextColor;
  final Color? selectedIconColor;
  final Color? textColor;
  final Color? iconColor;
  final String? text;
  final bool isSelected;
  final void Function()? onTap;
  final void Function(bool)? onHover;
  final void Function()? onLongPress;

  @override
  Widget build(BuildContext context) {
    return SMenuItem(
      style: style,
      child: AnimatedContainer(
        height: 45,
        margin: const EdgeInsets.only(top: 5),
        decoration: BoxDecoration(
          borderRadius: style.borderRadius,
          color: isSelected
              ? Theme.of(context).colorScheme.primary
              : style.bgColor ?? Theme.of(context).colorScheme.onPrimary,
        ),
        duration: const Duration(milliseconds: 250),
        child: TextButton(
          style: ButtonStyle(
              shape: MaterialStatePropertyAll(
                  RoundedRectangleBorder(borderRadius: style.borderRadius))),
          onPressed: onTap,
          onHover: onHover,
          onLongPress: onLongPress,
          child: Row(
            children: [
              Padding(
                padding: const EdgeInsets.all(0.0),
                child: Icon(
                  icon,
                  color: isSelected
                      ? selectedIconColor ??
                          Theme.of(context).colorScheme.onPrimary
                      : style.accentColor ??
                          iconColor ??
                          Theme.of(context).colorScheme.primary,
                ),
              ),
              Flexible(
                child: Padding(
                  padding: const EdgeInsets.only(left: 5),
                  child: Text(
                    text ?? '',
                    style: TextStyle(
                        color: isSelected
                            ? selectedTextColor ??
                                Theme.of(context).colorScheme.onPrimary
                            : style.accentColor ??
                                textColor ??
                                Theme.of(context).colorScheme.primary),
                    overflow: TextOverflow.fade,
                    maxLines: 1,
                    softWrap: false,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

const List<Map<String, dynamic>> data = [
  {
    'name': 'File 1',
    'created': '1/1/2000',
    'modified': '2/2/2023',
    'author': 'User 1',
    'type': 'svg',
    'location': '/usr/bin/mktemp',
    'size': '12.09 TB',
    'security': 'drwx------x',
  },
  {
    'name': 'File 2',
    'created': '2/2/2000',
    'modified': '3/3/2023',
    'author': 'User 2',
    'type': 'txt',
    'location': 'C:\\Windows\\Containers\\serviced',
    'size': '2 MB',
    'security': 'lrwxr-xr-x',
  },
  {
    'name': 'File 3',
    'created': '3/3/2000',
    'modified': '4/4/2023',
    'author': 'User 3',
    'type': 'dart',
    'location': '/User/User 3/bin/app.dart',
    'size': '6 GB',
    'security': '-rw-r--r--',
  },
  {
    'name': 'File 4',
    'created': '4/4/2000',
    'modified': '5/5/2023',
    'author': 'User 4',
    'type': 'pdf',
    'location': '/dev',
    'size': '15 KB',
    'security': '-rw-------',
  }
];
9
likes
150
points
41
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter package for all sorts of menus from dropdowns to sliding to resizable to navigation menus

Repository (GitHub)

Documentation

API reference

License

GPL-3.0 (license)

Dependencies

flutter

More

Packages that depend on flutter_smenus