Expandable FAB Example Blocks BuildContext in Flutter

Viewed 180

I created an expandable FAB button for my Flutter application with reading this Google Cookbook.

The control is it, same as Google version except Turkish typo:

import 'package:flutter/material.dart';
import 'dart:math' as math;

@immutable
class ExpandableFab extends StatefulWidget {
  const ExpandableFab({
    Key? key,
    this.initialOpen,
    required this.distance,
    required this.children,
  }) : super(key: key);

  final bool? initialOpen;
  final double distance;
  final List<Widget> children;

  @override
  _ExpandableFabState createState() => _ExpandableFabState();
}

class _ExpandableFabState extends State<ExpandableFab>
    with SingleTickerProviderStateMixin {
  late final Animation<double> _expandAnimation;
  bool _open = false;
  late final AnimationController _controller;

  final String moreText = 'İşlemleri görmek için basın.';

  final String closeText = 'Kapatmak için basın.';

  @override
  void initState() {
    super.initState();
    _open = widget.initialOpen ?? false;
    _controller = AnimationController(
      value: _open ? 1.0 : 0.0,
      duration: const Duration(milliseconds: 250),
      vsync: this,
    );
    _expandAnimation = CurvedAnimation(
      curve: Curves.fastOutSlowIn,
      reverseCurve: Curves.easeOutQuad,
      parent: _controller,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _toggle() {
    setState(() {
      _open = !_open;
      if (_open) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox.expand(
      child: Stack(
        alignment: Alignment.bottomRight,
        clipBehavior: Clip.none,
        children: [
          _buildTapToCloseFab(),
          ..._buildExpandingActionButtons(),
          _buildTapToOpenFab(),
        ],
      ),
    );
  }

  Widget _buildTapToCloseFab() {
    return SizedBox(
      width: 56.0,
      height: 56.0,
      child: Tooltip(
        message: closeText,
        child: Center(
          child: Material(
            shape: const CircleBorder(),
            clipBehavior: Clip.antiAlias,
            elevation: 4.0,
            child: InkWell(
              onTap: _toggle,
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Icon(
                  Icons.close,
                  color: Colors.grey,
                  size: 28,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  List<Widget> _buildExpandingActionButtons() {
    final children = <Widget>[];
    final count = widget.children.length;
    final step = 90.0 / (count - 1);
    for (var i = 0, angleInDegrees = 0.0;
        i < count;
        i++, angleInDegrees += step) {
      children.add(
        _ExpandingActionButton(
          directionInDegrees: angleInDegrees,
          maxDistance: widget.distance,
          progress: _expandAnimation,
          child: widget.children[i],
        ),
      );
    }
    return children;
  }

  Widget _buildTapToOpenFab() {
    return IgnorePointer(
      ignoring: _open,
      child: AnimatedContainer(
        transformAlignment: Alignment.center,
        transform: Matrix4.diagonal3Values(
          _open ? 0.7 : 1.0,
          _open ? 0.7 : 1.0,
          1.0,
        ),
        duration: const Duration(milliseconds: 250),
        curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
        child: AnimatedOpacity(
          opacity: _open ? 0.0 : 1.0,
          curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
          duration: const Duration(milliseconds: 250),
          child: FloatingActionButton(
            tooltip: moreText,
            onPressed: _toggle,
            child: const Icon(Icons.more_horiz),
          ),
        ),
      ),
    );
  }
}

@immutable
class _ExpandingActionButton extends StatelessWidget {
  _ExpandingActionButton({
    Key? key,
    required this.directionInDegrees,
    required this.maxDistance,
    required this.progress,
    required this.child,
  }) : super(key: key);

  final double directionInDegrees;
  final double maxDistance;
  final Animation<double> progress;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: progress,
      builder: (context, child) {
        final offset = Offset.fromDirection(
          directionInDegrees * (math.pi / 180.0),
          progress.value * maxDistance,
        );
        return Positioned(
          right: 4.0 + offset.dx,
          bottom: 4.0 + offset.dy,
          child: Transform.rotate(
            angle: (1.0 - progress.value) * math.pi / 2,
            child: child!,
          ),
        );
      },
      child: FadeTransition(
        opacity: progress,
        child: child,
      ),
    );
  }
}

@immutable
class ActionButton extends StatelessWidget {
  const ActionButton({
    Key? key,
    this.onPressed,
    required this.icon,
    required this.color,
    required this.tooltip,
  }) : super(key: key);

  final VoidCallback? onPressed;
  final Icon icon;
  final Color color;
  final String tooltip;
  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Material(
      shape: const CircleBorder(),
      clipBehavior: Clip.antiAlias,
      color: color,
      elevation: 4.0,
      child: IconTheme.merge(
        data: theme.accentIconTheme,
        child: IconButton(
          iconSize: 32,
          icon: icon,
          tooltip: tooltip,
          onPressed: () {
            onPressed!();
          },
        ),
      ),
    );
  }
}

@immutable
class FakeItem extends StatelessWidget {
  const FakeItem({
    Key? key,
    required this.isBig,
  }) : super(key: key);

  final bool isBig;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0),
      height: isBig ? 128.0 : 36.0,
      decoration: BoxDecoration(
        borderRadius: const BorderRadius.all(Radius.circular(8.0)),
        color: Colors.grey.shade300,
      ),
    );
  }
}

The UI state is it:

Animation

I use it as a FAB button on a Scaffold page like this:

  return Scaffold(
        floatingActionButton: ExpandableFab(
          distance: 112.0,
          children: [
            ActionButton(
              onPressed: () async {
                print('Tapped add user.');
                AddDialog addDialog = AddDialog(
                  primary: 'Kaydet',
                  secondary: 'İptal',
                  tooltip: 'Kullanıcı Ekle',
                  title: 'Kullanıcı Ekle',
                  key: Key('AddUser'),
                );
                bool result = await showDialog(
                  barrierDismissible: false,
                  context: this.context,
                  builder: (BuildContext context) {
                    return WillPopScope(
                      onWillPop: () async => false,
                      child: addDialog,
                    );
                  },
                );
                if (result) {
                  setState(() {});
                }
              },
              icon: const Icon(
                Icons.add,
                color: Colors.white,
              ),
              color: Theme.of(context).accentColor,
              tooltip: 'Kullanıcı Ekle',
            ),
            ActionButton(
              onPressed: () async {
                print("Tapped list all user's payments.");
                Navigator.push(
                    context,
                    MaterialPageRoute(
                        builder: (context) => Pays(
                              user: widget.loggedUser,
                              isAdmin:
                                  widget.loggedUser.type == 0 ? true : false,
                              isMe: true,
                              key: Key('UserPaysPage'),
                            )));
              },
              icon: const Icon(
                Icons.payments,
                color: Colors.white,
              ),
              color: Color.fromRGBO(
                  105, 82, 181, 1), //https://www.colorhexa.com/6952b5
              tooltip: 'Tüm Kullanıcı Ödemeleri',
            ),
            ActionButton(
              onPressed: () async {
                print("Tapped send user's access codes.");
                setState(() {
                  iswaiterVisible = !iswaiterVisible;
                });

                var allUsers = await User.getUsersFromDatabase();

                if (allUsers != null && allUsers.length != 0) {
                  var sendDetails = new Map(); // Sended or not recording.
                  for (var user in allUsers) {
                    bool sendResult =
                        await InOut.sendAccessCodetoCustomer(user);

                    sendResult == true
                        ? sendDetails['${Security.decryptMyData(user.email)}'] =
                            true
                        : sendDetails['${Security.decryptMyData(user.email)}'] =
                            false;
                  }
                  CustomSnackBar sentSnack = CustomSnackBar(
                      message:
                          'Gönderim raporu oluşturuluyor ! - Bu fonksiyon gönderimi simule eder, geçerliliği yoktur.',
                      type: AlertType.Info,
                      key: Key('ManageSnack-1'),
                      context: this.context);

                  sentSnack.showCustomSnackbar();
                } else {
                  CustomSnackBar errSnack = CustomSnackBar(
                      message:
                          'Erişim kodları gönderilirken bir hata oluştu ! - Bu fonksiyon gönderimi simule eder, geçerliliği yoktur.',
                      type: AlertType.Error,
                      key: Key('ManageSnack-2'),
                      context: this.context);

                  errSnack.showCustomSnackbar();
                }

                setState(() {
                  iswaiterVisible = !iswaiterVisible;
                });
              },

              icon: Icon(
                Icons.send,
                color: Colors.white,
              ),
              color: Color.fromRGBO(
                  181, 105, 82, 1), //https://www.colorhexa.com/b56952

              tooltip: 'Tüm Kullanıcılara Erişim Kodu Gönder',
            ),
          ],
          ......
        );

As you see in my code, I use CustomSnack class, which is a customized derive of SnackBar.

//Custom message snackbar class.
class CustomSnackBar {
  /// Constructor of custom snackbar. [message] is message, [type] is alert type [context] is context

  CustomSnackBar(
      {required Key key,
      required this.message,
      required this.type,
      required this.context});

  /// Message.
  final String message;

  ///Alert type
  final AlertType type;

  final BuildContext context;

  showCustomSnackbar() {
    var cstsnack = SnackBar(
      margin: EdgeInsets.only(left: 20, right: 20, bottom: 20),
      elevation: 10,
      padding: EdgeInsets.all(10),
      behavior: SnackBarBehavior.floating,
      shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(20),
          side: BorderSide(color: Color.fromRGBO(52, 52, 52, 0.5))),
      backgroundColor: (() {
        if (type == AlertType.Error) {
          return Colors.red[700]!.withOpacity(0.7);
        } else
          return type == AlertType.Warning
              ? Colors.orange[700]!.withOpacity(0.7)
              : Colors.green[700]!.withOpacity(0.7);
      }()),
      content: Row(
        children: [
          Padding(
            padding: EdgeInsets.symmetric(horizontal: 10),
            child: Tooltip(
              message: 'Alert icon.',
              child: Container(
                padding: EdgeInsets.all(5),
                decoration: ShapeDecoration(
                  shape: CircleBorder(),
                  color: Colors.blueGrey[700],
                ),
                child: Icon(
                  (() {
                    if (type == AlertType.Error) {
                      return Icons.error;
                    } else
                      return type == AlertType.Warning
                          ? Icons.warning
                          : Icons.check_circle;
                  }()),
                  size: 28,
                ),
              ),
            ),
          ),
          Flexible(
              fit: FlexFit.tight,
              child: Tooltip(
                message: message,
                child: Text(message,
                    style: (() {
                      return type == AlertType.Warning
                          ? Theme.of(context).textTheme.bodyText2
                          : Theme.of(context)
                              .textTheme
                              .bodyText2!
                              .copyWith(color: Colors.white);
                    }())),
              )),
          Padding(
            padding: EdgeInsets.symmetric(horizontal: 10),
            child: Tooltip(
                message: 'Kapat',
                child: IconButton(
                  icon: Icon(Icons.close),
                  onPressed: () {
                    ScaffoldMessenger.of(context).hideCurrentSnackBar();
                  },
                )),
          ),
        ],
      ),
    );

    ScaffoldMessenger.of(context).showSnackBar(cstsnack);
  }
}

Visualization of CustomSnackBar:

Visualization of CustomSnackBar

I can show this custom SnackBar everywhere with trigerring

ScaffoldMessenger.of(context).showSnackBar(cstsnack);

And it runs perfectly except the page expandable FAB is used. FAB control blocks the BuildContext in same page and custom SnackBar seems invisible. However, when I navigate the page, custom SnackBar comes from previous page state suddenly and make itself visible.

How can I fix this ?

Thanks.

0 Answers
Related