Flutterで折り畳み式のFloatingActionButtonを実装する(Speed Dialの実装)


Material Designで提唱されているSpeed Dialの実装について簡単に説明します。
<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351676/7558dec2-7838-8066-1e45-21f9d841d847.gif">

優秀なパッケージが既に用意されております。
[speed_dial 0.0.1](https://pub.dev/packages/speed_dial)
お急ぎの方は是非に

# 1.Preliminary

ScaffoldのFAB(FloatActionButton)について

``` dart
final Widget floatingActionButton;
```

で定義されております。

ご察しの通り、RowやColumnなどで様々なWidgetを
Scaffoldに入れいることができます。

[Flutterで2つのFloatingActionButtonを表示する方法](https://qiita.com/sekitaka_1214/items/351980e7b354c7e3859e)

尚、この記事はStackを利用して実装していきます。(捻りとは \:fearful:)

# 2.BlurBackground

<img width="240" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351676/b0fd4e5b-f2ba-78b9-2834-78620ad14e9f.gif">

``` dart
class FloatButtonBackground extends StatelessWidget {

  final Color color;

  FloatButtonBackground({this.color});

  @override
  Widget build(BuildContext context) {
    // TODO: implement buier
    return BackdropFilter(
      filter: ImageFilter.blur(sigmaX: 4.0, sigmaY: 4.0),
      child: Container(
        color: color,
      ),
    );
  }
}
```

# 3.FoldAnimation

<img width="120" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351676/5b805e1e-e074-c7d3-362d-86492795082e.gif">

``` dart
class FoldAnimationWrap extends StatefulWidget {

  final Duration duration;
  final Curve animationCurve;
  final bool isExpanded;

  final Widget child;

  FoldAnimationWrap({@required this.duration, this.animationCurve = Curves.ease,@required this.isExpanded, this.child});

  @override
  State<StatefulWidget> createState() {
    return _FoldAnimationWrap();
  }
}

class _FoldAnimationWrap extends State<FoldAnimationWrap> with TickerProviderStateMixin {

  AnimationController _controller;
  Animation<double> _expandAnimation;
  
  @override
  void initState() {
    _controller = AnimationController(
      duration: widget.duration,
      vsync: this,
      reverseDuration: widget.duration,
    );
    _expandAnimation = _controller.drive(CurveTween(curve: widget.animationCurve));
    if(widget.isExpanded){
      _controller.value = 1;
    }
  }

  @override
  void didUpdateWidget(FoldAnimationWrap oldWidget) {
    super.didUpdateWidget(oldWidget);
    if(oldWidget.isExpanded != widget.isExpanded) {
      if (widget.isExpanded) {
        _controller.forward();
      }
      else {
        _controller.reverse();
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _expandAnimation,
      child: widget.child,
    );
  }

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

# 4.FoldFloatButtonWrap

SpeedDialを構成するクラスになります。

``` dart
class FoldFloatButtonWrap extends StatelessWidget {

  final bool isExpanded;

  //** 常に表示されるFAB(Sampleのバーガーメニュー)　**//
  final Widget floatButton;

  //** 折り畳まれれるFAB　**//
  final List<Widget> expandedWidget;
  final Duration foldAnimationDuration = Duration(milliseconds: 400);

  FoldFloatButtonWrap({
    @required this.isExpanded,
    @required this.floatButton,
    @required this.expandedWidget,
  });

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      overflow: Overflow.visible,
      children: <Widget>[
        //** Background **//
        Positioned(
          top: 0,
          left: 0,
          right: -32,  //Scaffoldによるマージン
          bottom:  -32,
          child:FoldAnimationWrap(
            isExpanded: isExpanded,
            duration: foldAnimationDuration,
            child: FloatButtonBackground(color: Color.fromARGB(50, 0, 0, 0)),
          ),
        ),

        //** FAB **//
        Positioned(
          bottom: 0,
          right: 0,
          child: Column(
            children: <Widget>[
              FoldAnimationWrap(
                isExpanded: isExpanded,
                duration: foldAnimationDuration,
                child: Column(children: expandedWidget),
              ),
              floatButton,
            ],
          )
        ),
      ],
    );
  }
}
```

# 5.Implement Widget

Speed Dialの実装個所
既存のFABをFoldFloatButtonWrapで囲んで
最後にsetStateでisExpandedで制御すれば完成です。


``` dart
bool isExpanded = false;

@override
Widget build(BuildContext context) {
  return Scaffold(

    backgroundColor: ...,
    body: ...,

    floatingActionButton: 
      FoldFloatButtonWrap(
        isExpanded: isExpanded,
        floatButton: FloatingActionButton(
          child: isExpanded ? Icon(Icons.close) : Icon(Icons.menu) ,
          backgroundColor: Colors.deepOrangeAccent,
          onPressed: (){
            setState(() {
              isExpanded ^= true;
            });
          },
        ),
        expandedWidget: <Widget>[
          FloatingActionButton(
            child: Icon(Icons.content_copy, size: 16),
            backgroundColor: Colors.deepOrangeAccent,
            mini: true,
            onPressed: (){
              setState(() {
                isExpanded ^= true;
                text = "Copied";
              });
            },
          ),
          SizedBox(height: 4),
          FloatingActionButton(
            child: Icon(Icons.refresh,size: 16,),
            backgroundColor: Colors.deepOrangeAccent,
            mini: true,
            onPressed: (){
              setState(() {
                isExpanded ^= true;
                text = "Refrashed";
              });
            },
          ),
          SizedBox(height: 4),
          FloatingActionButton(
            child: Icon(Icons.delete,size: 16,),
            backgroundColor: Colors.deepOrangeAccent,
            mini: true,
            onPressed: (){
              setState(() {
                isExpanded ^= true;
                text = "Deleted";
              });
            },
          ),
          SizedBox(height: 8),
        ],
    ),
  );
}
```

# 5.最後に

画面制御やアニメーションなどとっつきにくいと感じて
手間を惜しんでパッケージをついつい利用しがちになりますが、
一歩踏み込んで自分なりに解釈して実装してみるのも一興かと思います。