FloatingActionButton - Animated FloatingActionButton in Flutter

Flutter offers two types of FloatingActionButtons out-of-the-box

  1. FloatingActionButton
  2. FloatingActionButton.extended

FloatingActionButton

The default constructor creates a simple circular FAB with a child widget inside it. It takes an onPressed method to react to taps and a child (not compulsory) to display a widget inside the FAB

 

FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),

FloatingActionButton

FloatingActionButton.extended

FloatingActionButton.extended offers a wide FAB, usually with an icon and a label inside it.

FloatingActionButton.extended(
onPressed: () {},
icon: Icon(Icons.save),
label: Text("Save"),
),

FloatingActionButtonExtend

How to Create AnimatedFloatingActionButton with Multiple Childs?

In the below example we are going to create Multiple FloatingAction buttons with HORIZONTAL and VERTICAL directions.

For this we are going to use UnicornButton dart class file.

 

Let's get Started

Step 1: Create flutter Application

Step 2: Create a class UnicornButton.dart and add below code

import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' as vector;

class UnicornOrientation {
  static const HORIZONTAL = 0;
  static const VERTICAL = 1;
}

class UnicornButton extends FloatingActionButton {
  final FloatingActionButton currentButton;
  final String labelText;
  final double labelFontSize;
  final Color labelColor;
  final Color labelBackgroundColor;
  final Color labelShadowColor;
  final bool labelHasShadow;
  final bool hasLabel;

  UnicornButton(
      {this.currentButton,
        this.labelText,
        this.labelFontSize = 14.0,
        this.labelColor,
        this.labelBackgroundColor,
        this.labelShadowColor,
        this.labelHasShadow = true,
        this.hasLabel = false})
      : assert(currentButton != null);

  Widget returnLabel() {
    return Container(
        decoration: BoxDecoration(
            boxShadow: this.labelHasShadow
                ? [
              new BoxShadow(
                color: this.labelShadowColor == null
                    ? Color.fromRGBO(204, 204, 204, 1.0)
                    : this.labelShadowColor,
                blurRadius: 3.0,
              ),
            ]
                : null,
            color: this.labelBackgroundColor == null
                ? Colors.white
                : this.labelBackgroundColor,
            borderRadius: BorderRadius.circular(3.0)), //color: Colors.white,
        padding: EdgeInsets.all(9.0),
        child: Text(this.labelText,
            style: TextStyle(
                fontSize: this.labelFontSize,
                fontWeight: FontWeight.bold,
                color: this.labelColor == null
                    ? Color.fromRGBO(119, 119, 119, 1.0)
                    : this.labelColor)));
  }


  Widget build(BuildContext context) {
    return this.currentButton;
  }
}

class UnicornDialer extends StatefulWidget {
  final int orientation;
  final Icon parentButton;
  final Icon finalButtonIcon;
  final bool hasBackground;
  final Color parentButtonBackground;
  final List<UnicornButton> childButtons;
  final int animationDuration;
  final int mainAnimationDuration;
  final double childPadding;
  final Color backgroundColor;
  final Function onMainButtonPressed;
  final Object parentHeroTag;
  final bool hasNotch;

  UnicornDialer(
      {this.parentButton,
        this.parentButtonBackground,
        this.childButtons,
        this.onMainButtonPressed,
        this.orientation = 1,
        this.hasBackground = true,
        this.backgroundColor = Colors.white30,
        this.parentHeroTag = "parent",
        this.finalButtonIcon,
        this.animationDuration = 180,
        this.mainAnimationDuration = 200,
        this.childPadding = 4.0,
        this.hasNotch = false})
      : assert(parentButton != null);

  _UnicornDialer createState() => _UnicornDialer();
}

class _UnicornDialer extends State<UnicornDialer>
    with TickerProviderStateMixin {
  AnimationController _animationController;
  AnimationController _parentController;

  bool isOpen = false;

  @override
  void initState() {
    this._animationController = AnimationController(
        vsync: this,
        duration: Duration(milliseconds: widget.animationDuration));

    this._parentController =
        AnimationController(vsync: this, duration: Duration(milliseconds: widget.mainAnimationDuration));

    super.initState();
  }

  @override
  dispose() {
    this._animationController.dispose();
    this._parentController.dispose();
    super.dispose();
  }

  void mainActionButtonOnPressed() {
    if (this._animationController.isDismissed) {
      this._animationController.forward();
    } else {
      this._animationController.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    this._animationController.reverse();

    var hasChildButtons =
        widget.childButtons != null && widget.childButtons.length > 0;

    if (!this._parentController.isAnimating) {
      if (this._parentController.isCompleted) {
        this._parentController.forward().then((s) {
          this._parentController.reverse().then((e) {
            this._parentController.forward();
          });
        });
      }
      if (this._parentController.isDismissed) {
        this._parentController.reverse().then((s) {
          this._parentController.forward();
        });
      }
    }

    var mainFAB = AnimatedBuilder(
        animation: this._parentController,
        builder: (BuildContext context, Widget child) {
          return Transform(
              transform: new Matrix4.diagonal3(vector.Vector3(
                  _parentController.value,
                  _parentController.value,
                  _parentController.value)),
              alignment: FractionalOffset.center,
              child: FloatingActionButton(
                  isExtended: false,
                  heroTag: widget.parentHeroTag,
                  backgroundColor: widget.parentButtonBackground,
                  onPressed: () {
                    mainActionButtonOnPressed();
                    if (widget.onMainButtonPressed != null) {
                      widget.onMainButtonPressed();
                    }
                  },
                  child: !hasChildButtons
                      ? widget.parentButton
                      : AnimatedBuilder(
                      animation: this._animationController,
                      builder: (BuildContext context, Widget child) {
                        return Transform(
                          transform: new Matrix4.rotationZ(
                              this._animationController.value * 0.8),
                          alignment: FractionalOffset.center,
                          child: new Icon(
                              this._animationController.isDismissed
                                  ? widget.parentButton.icon
                                  : widget.finalButtonIcon == null
                                  ? Icons.close
                                  : widget.finalButtonIcon.icon),
                        );
                      })));
        });

    if (hasChildButtons) {
      var mainFloatingButton = AnimatedBuilder(
          animation: this._animationController,
          builder: (BuildContext context, Widget child) {
            return Transform.rotate(
                angle: this._animationController.value * 0.8, child: mainFAB);
          });

      var childButtonsList = widget.childButtons == null ||
          widget.childButtons.length == 0
          ? List<Widget>()
          : List.generate(widget.childButtons.length, (index) {
        var intervalValue = index == 0
            ? 0.9
            : ((widget.childButtons.length - index) /
            widget.childButtons.length) -
            0.2;

        intervalValue =
        intervalValue < 0.0 ? (1 / index) * 0.5 : intervalValue;

        var childFAB = FloatingActionButton(
            onPressed: () {
              print("OnClicked");
              if (widget.childButtons[index].currentButton.onPressed !=
                  null) {
                widget.childButtons[index].currentButton.onPressed();
              }

              this._animationController.reverse();
            },
            child: widget.childButtons[index].currentButton.child,
            heroTag: widget.childButtons[index].currentButton.heroTag,
            backgroundColor:
            widget.childButtons[index].currentButton.backgroundColor,
            mini: widget.childButtons[index].currentButton.mini,
            tooltip: widget.childButtons[index].currentButton.tooltip,
            key: widget.childButtons[index].currentButton.key,
            elevation: widget.childButtons[index].currentButton.elevation,
            foregroundColor:
            widget.childButtons[index].currentButton.foregroundColor,
            highlightElevation: widget
                .childButtons[index].currentButton.highlightElevation,
            isExtended:
            widget.childButtons[index].currentButton.isExtended,
            shape: widget.childButtons[index].currentButton.shape);

        return Positioned(
          right: widget.orientation == UnicornOrientation.VERTICAL
              ? widget.childButtons[index].currentButton.mini ? 4.0 : 0.0
              : ((widget.childButtons.length - index) * 55.0) + 15,
          bottom: widget.orientation == UnicornOrientation.VERTICAL
              ? ((widget.childButtons.length - index) * 55.0) + 15
              : 8.0,
          child: Row(children: [
            ScaleTransition(
                scale: CurvedAnimation(
                  parent: this._animationController,
                  curve:
                  Interval(intervalValue, 1.0, curve: Curves.linear),
                ),
                alignment: FractionalOffset.center,
                child: (!widget.childButtons[index].hasLabel) ||
                    widget.orientation ==
                        UnicornOrientation.HORIZONTAL
                    ? Container()
                    : Container(
                    padding:
                    EdgeInsets.only(right: widget.childPadding),
                    child: widget.childButtons[index].returnLabel())),
            ScaleTransition(
                scale: CurvedAnimation(
                  parent: this._animationController,
                  curve:
                  Interval(intervalValue, 1.0, curve: Curves.linear),
                ),
                alignment: FractionalOffset.center,
                child: childFAB)
          ]),
        );
      });

      var unicornDialWidget = Container(
          margin: widget.hasNotch ? EdgeInsets.only(bottom: 15.0) : null,

          width: MediaQuery.of(context).size.width-100,
          child: Stack(
            //fit: StackFit.expand,
              alignment: Alignment.bottomRight,
              overflow: Overflow.visible,
              children: childButtonsList.toList()
                ..add(Positioned(
                    right: null, bottom: null, child: mainFloatingButton))));

      var modal = ScaleTransition(
          scale: CurvedAnimation(
            parent: this._animationController,
            curve: Interval(1.0, 1.0, curve: Curves.linear),
          ),
          alignment: FractionalOffset.center,
          child: GestureDetector(
              onTap: mainActionButtonOnPressed,
              child: Container(
                color: widget.backgroundColor,
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
              )));

      return widget.hasBackground
          ? Stack(
          alignment: Alignment.topCenter,
          overflow: Overflow.visible,
          children: [
            Positioned(right: -16.0, bottom: -16.0, child: modal),
            unicornDialWidget
          ])
          : unicornDialWidget;
    }

    return mainFAB;
  }
}

 

Step 3: Update main dart file

import 'dart:ui';

import 'package:animated_floatactionbuttons/animated_floatactionbuttons.dart';
import 'package:flutter/material.dart';

import 'UnicornButton.dart';

void main() => runApp(MaterialApp(home: CanvasPainting(),));

class CanvasPainting extends StatefulWidget {
  @override
  _CanvasPaintingState createState() => _CanvasPaintingState();
}

class _CanvasPaintingState extends State<CanvasPainting> {

  static final List lineWidths = [3.0, 5.0, 8.0];
  // File imageFile;
  int selectedLine = 0;

  StrokeCap strokeType = StrokeCap.round;
  static double strokeWidth = 3.0;
  static Color selectedColor = Colors.black;
  int curFrame = 0;
  bool isClear = false;
  double opacity = 1.0;

  List points = [Point(selectedColor, strokeWidth, [])];
  Future<void> _pickStroke() async {
    //Shows AlertDialog
    return showDialog<void>(
      context: context,

      //Dismiss alert dialog when set true
      barrierDismissible: true, // user must tap button!
      builder: (BuildContext context) {
        //Clips its child in a oval shape
        return ClipOval(
          child: AlertDialog(
            //Creates three buttons to pick stroke value.
            actions: <Widget>[
              //Resetting to default stroke value
              FlatButton(
                child: Icon(
                  Icons.clear,
                ),
                onPressed: () {
                  strokeWidth = 3.0;
                  Navigator.of(context).pop();
                },
              ),
              FlatButton(
                child: Icon(
                  Icons.brush,
                  size: 24,
                ),
                onPressed: () {
                  strokeWidth = 10.0;
                  Navigator.of(context).pop();
                },
              ),
              FlatButton(
                child: Icon(
                  Icons.brush,
                  size: 40,
                ),
                onPressed: () {
                  strokeWidth = 30.0;
                  Navigator.of(context).pop();
                },
              ),
              FlatButton(
                child: Icon(
                  Icons.brush,
                  size: 60,
                ),
                onPressed: () {
                  strokeWidth = 50.0;
                  Navigator.of(context).pop();
                },
              ),
            ],
          ),
        );
      },
    );
  }

  Future<void> _opacity() async {
    //Shows AlertDialog
    return showDialog<void>(
      context: context,

      //Dismiss alert dialog when set true
      barrierDismissible: true,

      builder: (BuildContext context) {
        //Clips its child in a oval shape
        return ClipOval(
          child: AlertDialog(
            //Creates three buttons to pick opacity value.
            actions: <Widget>[
              FlatButton(
                child: Icon(
                  Icons.opacity,
                  size: 24,
                ),
                onPressed: () {
                  //most transparent
                  opacity = 0.1;
                  Navigator.of(context).pop();
                },
              ),
              FlatButton(
                child: Icon(
                  Icons.opacity,
                  size: 40,
                ),
                onPressed: () {
                  opacity = 0.5;
                  Navigator.of(context).pop();
                },
              ),
              FlatButton(
                child: Icon(
                  Icons.opacity,
                  size: 60,
                ),
                onPressed: () {
                  //not transparent at all.
                  opacity = 1.0;
                  Navigator.of(context).pop();
                },
              ),
            ],
          ),
        );
      },
    );
  }

  paintOptions()
  {
    return UnicornDialer(
        backgroundColor: Color.fromRGBO(255, 255, 255, 0.6),
        parentButtonBackground: Colors.redAccent,
        orientation: UnicornOrientation.HORIZONTAL,
        parentButton: Icon(Icons.more),
        onMainButtonPressed: (){
          print("OnClicked Parent");
        },
        childButtons: [

          UnicornButton(

            currentButton: FloatingActionButton(
              heroTag: "paint_stroke",
              backgroundColor: Colors.teal,
              child: Icon(Icons.brush),
              tooltip: 'Stroke',
              onPressed: () {
                print("OnClicked");
                //min: 0, max: 50
                setState(() {
                  _pickStroke();
                });
              },
            ),
          ),

          UnicornButton(
            currentButton: FloatingActionButton(
              backgroundColor: Color(0XFF8E44AD),
              heroTag: "paint_opacity",
              child: Icon(Icons.opacity),
              tooltip: 'Opacity',
              onPressed: () {
                //min:0, max:1
                setState(() {
                  _opacity();
                });
              },
            ),
          ),
          UnicornButton(
            currentButton: FloatingActionButton(
                backgroundColor: Color(0XFFF39C12),
                heroTag: "erase",
                child: Icon(Icons.clear),
                tooltip: "Erase",
                onPressed: () {
                  setState(() {
                    points.clear();
                    curFrame=0;
                    points = [Point(selectedColor, strokeWidth, [])];
                  });
                }),
          ),
        ]);
  }

  getColorFAB()
  {
    return UnicornDialer(
        backgroundColor: Color.fromRGBO(255, 255, 255, 0.6),
            parentButtonBackground: Colors.redAccent,
            orientation: UnicornOrientation.HORIZONTAL,
            parentButton: Icon(Icons.view_list),
            onMainButtonPressed: (){
            print("OnClicked Parent");
       },
        childButtons: [
          UnicornButton(
            currentButton: FloatingActionButton(
              backgroundColor: Colors.white,
              heroTag: "color_red",
              child: colorMenuItem(Colors.red),
              tooltip: 'Color',
              onPressed: () {
                setState(() {
                  selectedColor = Colors.red;
                });
              },
            ),
          ),
          UnicornButton(
            currentButton: FloatingActionButton(
              backgroundColor: Colors.white,
              heroTag: "color_green",
              child: colorMenuItem(Colors.green),
              tooltip: 'Color',
              onPressed: () {
                setState(() {
                  selectedColor = Colors.green;
                });
              },
            ),
          ),
          UnicornButton(
            currentButton: FloatingActionButton(
              backgroundColor: Colors.white,
              heroTag: "color_pink",
              child: colorMenuItem(Colors.pink),
              tooltip: 'Color',
              onPressed: () {
                setState(() {
                  selectedColor = Colors.pink;
                });
              },
            ),
          ),
          UnicornButton(
            currentButton: FloatingActionButton(
              backgroundColor: Colors.white,
              heroTag: "color_blue",
              child: colorMenuItem(Colors.blue),
              tooltip: 'Color',
              onPressed: () {
                setState(() {
                  selectedColor = Colors.blue;
                });
              },
            ),
          ),
        ],
    );

  }


  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: SafeArea(
        child: Scaffold(
          body:  Card(
            color: Colors.blueGrey,
            elevation: 10,
            clipBehavior: Clip.hardEdge,
            child: Container(
              margin: EdgeInsets.all(10),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.all(Radius.circular(10)),
                      color: Colors.white
                ),
                child: _buildCanvas()),
          ),
          floatingActionButton: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            mainAxisSize: MainAxisSize.max,
            crossAxisAlignment: CrossAxisAlignment.end,
            children: <Widget>[
              paintOptions(),
         SizedBox(height: 10,),
              getColorFAB()
        ,],)
        ),
      ),
    );
  }

  Widget colorMenuItem(Color color) {
    return GestureDetector(
      onTap: () {
        setState(() {
          selectedColor = color;
        });
      },
      child: ClipOval(
        child: Container(
          padding: const EdgeInsets.only(bottom: 8.0),
          height: 36,
          width: 36,
          color: color,
        ),
      ),
    );
  }

  final List colors = [
    Colors.black,
    Colors.purple,
    Colors.green,
  ];
  Widget _buildCanvas() {
    return StatefulBuilder(builder: (context, state) {
      return CustomPaint(
        painter: CustomePainter(
          points: points,
          strokeColor: selectedColor,
          strokeWidth: strokeWidth,
          isClear: isClear,
        ),
        child: GestureDetector(
          onPanStart: (details) {
            // before painting, set color & strokeWidth.
            isClear = false;
            points[curFrame].color = selectedColor;
            points[curFrame].strokeWidth = strokeWidth;
          },
          onPanUpdate: (details) {
            RenderBox referenceBox = context.findRenderObject();
            Offset localPosition =
            referenceBox.globalToLocal(details.globalPosition);
            state(() {
              points[curFrame].points.add(localPosition);
            });
          },
          onPanEnd: (details) {
            // preparing for next line painting.
            points.add(Point(selectedColor, strokeWidth, []));
            curFrame++;
          },
        ),
      );
    });


  }



}
class Point {
  Color color;
  List points;
  double strokeWidth = 5.0;

  Point(this.color, this.strokeWidth, this.points);
}
class CustomePainter extends CustomPainter {

  final double strokeWidth;
  final Color strokeColor;
  Paint _linePaint;
  final bool isClear;
  final List points;

  CustomePainter({
    @required this.points,
    @required this.strokeColor,
    @required this.strokeWidth,
    this.isClear = true,
  }) {
    _linePaint = Paint()
      ..color = strokeColor
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round;
  }

  void paint(Canvas canvas, Size size) {
    if (isClear || points == null || points.length == 0) {
      return;
    }
    for (int i = 0; i < points.length; i++) {
      _linePaint..color = points[i].color;
      _linePaint..strokeWidth = points[i].strokeWidth;
      List curPoints = points[i].points;
      if (curPoints == null || curPoints.length == 0) {
        break;
      }
      for (int i = 0; i < curPoints.length - 1; i++) {
        if (curPoints[i] != null && curPoints[i + 1] != null)
          canvas.drawLine(curPoints[i], curPoints[i + 1], _linePaint);
//      canvas.drawPoints(PointMode.polygon, curPoints, _linePaint);
      }
    }
  }

  bool shouldRepaint(CustomePainter other) => true;
}
/*class MyPainter extends CustomPainter {
  MyPainter({this.pointsList});

  //Keep track of the points tapped on the screen
  List<TouchPoints> pointsList;
  List<Offset> offsetPoints = List();

  //This is where we can draw on canvas.
  @override
  void paint(Canvas canvas, Size size) {
    for (int i = 0; i < pointsList.length - 1; i++) {
      if (pointsList[i] != null && pointsList[i + 1] != null) {
        //Drawing line when two consecutive points are available
        canvas.drawLine(pointsList[i].points, pointsList[i + 1].points,
            pointsList[i].paint);
      } else if (pointsList[i] != null && pointsList[i + 1] == null) {
        offsetPoints.clear();
        offsetPoints.add(pointsList[i].points);
        offsetPoints.add(Offset(
            pointsList[i].points.dx + 0.1, pointsList[i].points.dy + 0.1));

        //Draw points when two points are not next to each other
        canvas.drawPoints(PointMode.points, offsetPoints, pointsList[i].paint);
      }
    }
  }

  //Called when CustomPainter is rebuilt.
  //Returning true because we want canvas to be rebuilt to reflect new changes.
  @override
  bool shouldRepaint(MyPainter oldDelegate) => true;
}*/


 

Step 4: Run application

AnimatedFloatingActionButton

 

AnimatedFloatingActionButton