'Flutter - How to Make floating action button animation like gmail?

I am able to make quite a similar floating action button animation like Gmail app, but I am getting a little bit of margin when I isExpanded is false. Any solution?

Here is my code

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool isExpanded = false;

  Widget build(context) {
    return Scaffold(
        floatingActionButton: AnimatedContainer(
          width: isExpanded ? 150 : 56,
          height: 56,
          duration: Duration(milliseconds: 300),
          child: FloatingActionButton.extended(
            onPressed: () {},
            icon: Icon(Icons.ac_unit),
            label: isExpanded ? Text("Start chat") : SizedBox(),
          ),
        ),
        appBar: AppBar(),
        body: FlatButton(
            onPressed: () {
              setState(() {
                isExpanded = !isExpanded;
              });
            },
            child: Text('Press here to change FAB')));
  }
}


Solution 1:[1]

Looks like FloatingActionButton has some hardcoded padding set for an icon. To fix that, you could do the following:

FloatingActionButton.extended(
  onPressed: () {},
  icon: isExpanded ? Icon(Icons.ac_unit) : null,
  label: isExpanded ? Text("Start chat") : Icon(Icons.ac_unit),
)

Solution 2:[2]

If you want an animation then you have to write your own custom fab:

class ScrollingExpandableFab extends StatefulWidget {
  const ScrollingExpandableFab({
    Key? key,
    this.controller,
    required this.label,
    required this.icon,
    this.onPressed,
    this.scrollOffset = 50.0,
    this.animDuration = const Duration(milliseconds: 500),
  }) : super(key: key);

  final ScrollController? controller;

  final String label;

  final Widget icon;

  final VoidCallback? onPressed;

  final double scrollOffset;

  final Duration animDuration;

  @override
  State<ScrollingExpandableFab> createState() => _ScrollingExpandableFabState();
}

class _ScrollingExpandableFabState extends State<ScrollingExpandableFab>
    with TickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    duration: widget.animDuration,
    vsync: this,
  );

  late final Animation<double> _anim = Tween<double>(begin: 0.0, end: 1.0)
      .animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));

  Color get _backgroundColor =>
      Theme.of(context).floatingActionButtonTheme.backgroundColor ??
      Theme.of(context).colorScheme.secondary;

  ScrollController? get _scrollController => widget.controller;

  _scrollListener() {
    final position = _scrollController!.position;
    if (position.pixels > widget.scrollOffset &&
        position.userScrollDirection == ScrollDirection.reverse) {
      _controller.forward();
    } else if (position.pixels <= widget.scrollOffset &&
        position.userScrollDirection == ScrollDirection.forward) {
      _controller.reverse();
    }
  }

  @override
  void initState() {
    super.initState();
    _scrollController?.addListener(_scrollListener);
  }

  @override
  void dispose() {
    _scrollController?.removeListener(_scrollListener);
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: const BoxConstraints.tightFor(height: 48.0),
      child: AnimatedBuilder(
        animation: _anim,
        builder: (context, child) => Material(
          elevation: 4.0,
          type: MaterialType.button,
          color: _backgroundColor,
          shape: const CircleBorder(),
          clipBehavior: Clip.antiAlias,
          child: InkWell(
            onTap: widget.onPressed,
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16.0),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  widget.icon,
                  ClipRect(
                    child: Align(
                      alignment: AlignmentDirectional.centerStart,
                      widthFactor: 1 - _anim.value,
                      child: Opacity(
                        opacity: 1 - _anim.value,
                        child: Padding(
                          padding: const EdgeInsets.only(left: 8.0),
                          child: SimpleclubText.button(widget.label),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2