showBottomActionSheet<T> function

Future<T?> showBottomActionSheet<T>({
  1. required BuildContext context,
  2. required List<Widget> children,
  3. required List<VoidCallback?> actions,
  4. List<Text?>? descriptions,
  5. EdgeInsetsGeometry descriptionsPadding = const EdgeInsets.only(top: 5),
  6. Text? titleText,
  7. EdgeInsetsGeometry titlePadding = const EdgeInsets.only(top: 5, bottom: 35),
  8. WidgetPositioning widgetPositioning = WidgetPositioning.leftBound,
  9. BorderRadius? widgetBorderRadius,
  10. Color? widgetBorderColor,
  11. int? maxPerRow,
  12. double widgetBorderSize = 24.0,
  13. double? widgetSplashRadius,
  14. double? widgetBorderWidth = 1,
  15. EdgeInsetsGeometry widgetPadding = const EdgeInsets.all(8.0),
  16. double? rowGap = 14.0,
  17. MainAxisAlignment? mainAxisAlignment = MainAxisAlignment.spaceAround,
  18. EdgeInsetsGeometry sheetPadding = const EdgeInsets.fromLTRB(20, 20, 20, 15),
  19. BoxConstraints? constraints,
  20. VisualDensity? visualDensity,
  21. Color? backgroundColor,
  22. double? elevation,
  23. ShapeBorder shape = const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
  24. Clip clipBehaviour = Clip.antiAliasWithSaveLayer,
  25. Color? barrierColor,
  26. bool isScrollControlled = false,
  27. bool useRootNavigator = false,
  28. bool isDismissible = true,
  29. bool enableDrag = true,
  30. RouteSettings? routeSettings,
  31. AnimationController? transitionAnimationController,
})

Shows a material design bottom sheet with actions.

It is based on Flutter's Bottom Sheet providing an easy template for using it with actions.

A modal bottom sheet is an alternative to a menu or a dialog and prevents the user from interacting with the rest of the app.

A closely related widget is a persistent bottom sheet, which shows information that supplements the primary content of the app without preventing the use from interacting with the app. Persistent bottom sheets can be created and displayed with the showBottomSheet function or the ScaffoldState.showBottomSheet method.

The children argument specifies all widgets/actions to be displayed in the bottom sheet.

The actions argument specifies the function for each widget/action.

The descriptions parameter can be used to give actions a Text subtitle to describe it.

The 'title' parameter is used to display a given Text as a title or heading for the bottom action sheet.

The titlePadding parameter defines the padding around a given title.

The widgetPositioning parameter is used to specify how the widgets/actions in each row are laid out.

The maxPerRow parameter defines the maximum amount of widgets/actions per row. If no value is provided it tries to choose to most suitable one.

The widgetBorderSize parameter specifies the width and height used for every widget/action. Defaults to 24.0

The widgetPadding defines the padding used around every widget/action. Defaults to EdgeInsets.all(8.0).

The rowGap is used to define to gap between each row. Defaults to 14.0

The mainAxisAlignment specifies the mainAxisAlignment for every row of widgets/actions shown.

The sheetPadding defines the padding surrounding the action sheet. Defaults to EdgeInsets.fromLTRB(20, 20, 20, 15).

The context argument is used to look up the Navigator and Theme for the bottom sheet. It is only used when the method is called. Its corresponding widget can be safely removed from the tree before the bottom sheet is closed.

The isScrollControlled parameter specifies whether this is a route for a bottom sheet that will utilize DraggableScrollableSheet. If you wish to have a bottom sheet that has a scrollable child such as a ListView or a GridView and have the bottom sheet be draggable, you should set this parameter to true.

The useRootNavigator parameter ensures that the root navigator is used to display the BottomSheet when set to true. This is useful in the case that a modal BottomSheet needs to be displayed above all other content but the caller is inside another Navigator.

The isDismissible parameter specifies whether the bottom sheet will be dismissed when user taps on the scrim.

The enableDrag parameter specifies whether the bottom sheet can be dragged up and down and dismissed by swiping downwards.

The optional backgroundColor, elevation, shape, clipBehavior, constraints and transitionAnimationController parameters can be passed in to customize the appearance and behavior of modal bottom sheets (see the documentation for these on BottomSheet for more details).

The transitionAnimationController controls the bottom sheet's entrance and exit animations if provided.

The optional routeSettings parameter sets the RouteSettings of the modal bottom sheet sheet. This is particularly useful in the case that a user wants to observe PopupRoutes within a NavigatorObserver.

Returns a Future that resolves to the value (if any) that was passed to Navigator.pop when the modal bottom sheet was closed.

Widget build(BuildContext context) {
   return Center(
     child: ElevatedButton(
       child: Text("showBottomActionSheet"),
       onPressed: () {
         showBottomActionSheet(
           context: context,
           widgetPositioning: WidgetPositioning.mainAxis,
           children: [
             Icon(
               Icons.class_,
               color: Colors.white,
             ),
             Icon(
               Icons.folder,
               color: Colors.white,
             ),
             Icon(
               Icons.note_add,
               color: Colors.white,
             ),
           ],
           descriptions: [
             Text("Class"),
             Text("Folder"),
             Text("Note"),
           ],
           actions: [],
           titleText: Text(
             "Create New",
             style: TextStyle(fontSize: 25),
           ),
         );
       },
     ),
   );
 }

Implementation

Future<T?> showBottomActionSheet<T>({
  required BuildContext context,
  required List<Widget> children,
  required List<VoidCallback?> actions,
  List<Text?>? descriptions,
  EdgeInsetsGeometry descriptionsPadding = const EdgeInsets.only(top: 5),
  Text? titleText,
  EdgeInsetsGeometry titlePadding = const EdgeInsets.only(top: 5, bottom: 35),
  WidgetPositioning widgetPositioning = WidgetPositioning.leftBound,
  BorderRadius? widgetBorderRadius,
  Color? widgetBorderColor,
  int? maxPerRow,
  double widgetBorderSize = 24.0,
  double? widgetSplashRadius,
  double? widgetBorderWidth = 1,
  EdgeInsetsGeometry widgetPadding = const EdgeInsets.all(8.0),
  double? rowGap = 14.0,
  MainAxisAlignment? mainAxisAlignment = MainAxisAlignment.spaceAround,
  EdgeInsetsGeometry sheetPadding = const EdgeInsets.fromLTRB(20, 20, 20, 15),
  BoxConstraints? constraints,
  VisualDensity? visualDensity,
  Color? backgroundColor,
  double? elevation,
  ShapeBorder shape = const RoundedRectangleBorder(
    borderRadius: BorderRadius.vertical(
      top: Radius.circular(16),
    ),
  ),
  Clip clipBehaviour = Clip.antiAliasWithSaveLayer,
  Color? barrierColor,
  bool isScrollControlled = false,
  bool useRootNavigator = false,
  bool isDismissible = true,
  bool enableDrag = true,
  RouteSettings? routeSettings,
  AnimationController? transitionAnimationController,
}) {
  /// Minimum logical pixel size of the IconButton.
  const double _kMinButtonSize = kMinInteractiveDimension;

  final VisualDensity effectiveVisualDensity =
      visualDensity ?? Theme.of(context).visualDensity;

  final BoxConstraints unadjustedConstraints = constraints ??
      const BoxConstraints(
        minWidth: _kMinButtonSize,
        minHeight: _kMinButtonSize,
      );

  final BoxConstraints adjustedConstraints =
      effectiveVisualDensity.effectiveConstraints(unadjustedConstraints);

  /// Automatically set [maxPerRow] to provide a good layout, unless a custom value is given.
  /// Max Widgets - 5 , Min Widgets - 2
  /// Chooses a value where [neededRows] * [maxPerRow] = number of Children, if possible.
  /// If mutiple values can apply the smallest number that is greater than [neededRows] is choosen.
  /// If no value applies, the smallest number that is still greater than [neededRows] is choosen.
  if (maxPerRow == null) {
    for (var nmb = 5; nmb > 1; nmb--) {
      if (children.length % nmb == 0) {
        if ((children.length / nmb).ceil() <= nmb) {
          maxPerRow = nmb;
        }
      }
    }
    if (maxPerRow == null) {
      for (var nmb = 5; nmb > 1; nmb--) {
        if ((children.length / nmb).ceil() <= nmb) {
          maxPerRow = nmb;
        }
      }
    }
  }

  /// Number of Rows needed to fit every given Widget according to [maxPerRow].
  int neededRows = (children.length / maxPerRow!).ceil();

  /// Number of (non visible) Widgets missing to maintain Layout.
  /// = Rows needed * max Items per Row [maxPerRow] - given Widgets [childre.length].
  int missingWidgets = neededRows * maxPerRow - children.length;

  /// When no descriptions have been defined, given Widgets will be given a null value.
  /// This results in no descriptions at all.
  if (descriptions == null) {
    descriptions = [];
    for (var i = 0; i < children.length; i++) {
      descriptions.add(null);
    }
  }

  /// Alters [children], [actions] and [descriptions] to match WidgetPositioning.mainAxis.
  if (widgetPositioning == WidgetPositioning.leftBound) {
    /// When the amount of given Widgets doesn't match the needed amount,
    /// add "Invisible" Container.
    /// Amount specified by [missingWidgets].
    if (children.length < neededRows * maxPerRow) {
      for (var i = 0; i < missingWidgets; i++) {
        children.add(Container());
      }
    }

    /// When no actinos have been specified, add empty funtion bodys.
    /// This is in order to make the button clickable even without purpose.
    /// Empty function bodys will be added for every given Widget [children.length].
    if (actions.length == 0) {
      for (var i = 0; i < children.length - missingWidgets; i++) {
        actions.add(() {});
      }
    }

    /// When some actions are given but not enough, every Widget in [children] receives a value of null.
    /// This also applies to the potentially added Containers in order to avoid them being clickable.
    if (actions.length < children.length) {
      for (var i = 0; i < missingWidgets; i++) {
        actions.add(null);
      }
    }

    /// When some descriptions have been defined but not for ever given Widget,
    /// the remaining Widgets will recevĂ­ve a null value, in order to not show anything.
    if (descriptions.length < children.length) {
      for (var i = 0; i < children.length - descriptions.length + i; i++) {
        descriptions.add(null);
      }
    }
  }

  /// Alters [children], [actions] and [descriptions] to match WidgetPositioning.leftBound.
  else {
    for (var i = 0; i < children.length; i++) {
      actions.add(() {});

      descriptions.add(null);
    }
  }

  /// List with Widget-Sublists for every Row.
  final List<List<Widget>> widgetList = [];

  /// Sublist with Widgets per Row.
  List<Widget> widgets = [];

  /// List with Actions/Function-Sublists for every Row.
  final List<List<VoidCallback?>> onPressedList = [];

  /// Sublist with Actions/Functions per Row.
  List<VoidCallback?> onPressed = [];

  /// List with Description-Sublists for every Row.
  final List<List<Text?>> textList = [];

  /// Sublist with Descriptions per Row.
  List<Text?> texts = [];

  /// Count till [maxPerRow] has been reached.
  /// Used to generate List with Widgets of each row.
  /// Gets reset afterwards.
  int index = 0;

  /// For each parameter [children], [actions] and [descriptions], Lists are created.
  /// These Lists contain a Sublist for every row and contain the corresponding Information.
  /// It loops through the indices 0 to [children.length].
  /// Seperately the variable [index] is increased till it reaches [maxPerRow].
  /// After it hits its maximum the Sublist gets added to the final Lists and [index] is reset.
  for (var i = 0; i < children.length; i++) {
    if (index <= maxPerRow) {
      index += 1;
      widgets.add(children[i]);

      onPressed.add(actions[i]);

      texts.add(descriptions[i]);
      if (index == maxPerRow) {
        widgetList.addAll([widgets]);
        widgets = [];

        onPressedList.addAll([onPressed]);
        onPressed = [];

        textList.addAll([texts]);
        texts = [];
        index = 0;
      }
    }
  }

  /// Adds the Sublists, that haven't been added because they have less Widgets than [maxPerRow] to their accroding main List.
  if (widgetPositioning == WidgetPositioning.mainAxis) {
    widgetList.addAll([widgets]);

    onPressedList.addAll([onPressed]);

    textList.addAll([texts]);
  }

  /// Contains every Row to be shown.
  List<Widget> rowList = [];

  /// Creates a new Row for every needed Row.
  /// Every Row also returns a Column including all Widgets from the Sublist,
  /// according to [row] and [widgetIndex].
  /// A border is only shown if a function has been provided through [actions].
  /// Unless given the BorderColor defaults to black or white depending on [Brightness].
  /// Unless a custom [splashRadius] has been given, [widgetBorderSize] + 5 is used for splashes.
  /// If no [descriptions] have been defined an empty Container is returned, making it invisible onscreen.
  /// After every Row except the last a spacer is returned. Unless given its height [rowGap] defaults to 14.0.
  for (var row = 0; row < neededRows; row++) {
    rowList.add(
      Container(
        child: Row(
          mainAxisAlignment: mainAxisAlignment!,
          // Widgets always on same baseline
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            for (var widgetIndex = 0;
                widgetIndex < widgetList[row].length;
                widgetIndex++)
              Column(
                children: [
                  Container(
                    decoration: BoxDecoration(
                        border: onPressedList[row][widgetIndex] != null
                            ? Border.all(
                                width: widgetBorderWidth!,
                                color: widgetBorderColor ??=
                                    Theme.of(context).brightness ==
                                            Brightness.dark
                                        ? Colors.white
                                        : Colors.black)
                            : null,
                        borderRadius:
                            widgetBorderRadius ?? BorderRadius.circular(90)),
                    child: ConstrainedBox(
                      constraints: adjustedConstraints,
                      child: Padding(
                        padding: widgetPadding,
                        child: SizedBox(
                          height: widgetBorderSize,
                          width: widgetBorderSize,
                          child: InkResponse(
                            child: Center(
                              child: widgetList[row][widgetIndex],
                            ),
                            onTap: onPressedList[row][widgetIndex],
                            radius: widgetSplashRadius ?? widgetBorderSize + 5,
                          ),
                        ),
                      ),
                    ),
                  ),
                  Container(
                    padding: descriptionsPadding,
                    child: onPressedList[row][widgetIndex] != null
                        ? textList[row][widgetIndex]
                        : Container(),
                  ),
                ],
              ),
          ],
        ),
      ),
    );

    if (row < neededRows - 1)
      rowList.add(SizedBox(
        height: rowGap,
      ));
  }

  /// Shows the actual bottom sheet
  /// Returns container to enable custom padding, defaults to EdgeInsets.fromLTRB(20, 20, 20, 15).
  /// The Column contains every given Row in [rowList].
  /// The mainAxisSize is set to be minimal in order to maintain the layout.
  /// When defined a titel [titleText] with [titelPadding] for the sheet will be displayed.
  return showModalBottomSheet<T>(
    context: context,
    backgroundColor: backgroundColor,
    elevation: elevation,
    shape: shape,
    clipBehavior: clipBehaviour,
    barrierColor: barrierColor,
    isScrollControlled: isScrollControlled,
    useRootNavigator: useRootNavigator,
    isDismissible: isDismissible,
    enableDrag: enableDrag,
    routeSettings: routeSettings,
    transitionAnimationController: transitionAnimationController,
    builder: (BuildContext context) {
      return Container(
        padding: sheetPadding,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            if (titleText != null)
              Container(
                padding: titlePadding,
                child: titleText,
              ),
            for (var row in rowList) row,
          ],
        ),
      );
    },
  );
}