How to align widgets in Flutter: Part 1

Share this post

You can find Part 2 here

I remember when I started using Flutter, there were a lot of use cases in terms of alignment, where I didn’t know exactly how to approach them. I think alignment of widgets is one of the most important parts in Flutter UI (as it is in web development) and Flutter made it terribly easy to do so, given that you know the right widgets/approach to use.

In this series, I won’t go very deep into the technical details of each widget that I describe but rather will give practical examples with use cases.

How to align single widgets in Flutter

For this use case, we can use the Align widget which lets us align our widget in 9 places in the parent widget:

  • topLeft
  • topCenter
  • topRight
  • centerLeft
  • center
  • centerRight
  • bottomLeft
  • bottomCenter
  • bottomRight

We just need to define what alignment we want and what widget we want to align. In this case, we choose for Alignment.topCenter and we are aligning a Container with a Text as the child.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Align(
        alignment: Alignment.topCenter,
        child: Container(
          color: Colors.lightBlue,
          child: Text(
            'mrflutter.com',
            style: TextStyle(
              fontSize: 30,
            ),
          ),
        ),
      ),
    );
  }

Which will obviously look like this:

Single widget alignment in Flutter
Alignment.topCenter

Or we could try Alignment.centerLeft, which will look like this:

Single widget alignment in Flutter
Alignment.centerLeft

You can try other variants of alignment and see what happens. I think you get the idea. For Alignment.center, there is a shortcut available and that will be a bridge to our next use case.

How to center a widget horizontally and vertically in Flutter

There is a very useful widget called Center that takes a child widget and aligns it in the center of the parent widget. This works exactly the same as the Align widget with Alignment.center parameter.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Container(
          color: Colors.lightBlue,
          child: Text(
            'mrflutter.com',
            style: TextStyle(
              fontSize: 30,
            ),
          ),
        ),
      ),
    );
  }

Fullscreen page with transparent AppBar in Flutter

Share this post

Sometimes one wants to build a completely fullscreen experience with or without an image background. There are 2 ways to do this:

  • Without using an AppBar and use SafeArea to keep our UI elements inside the ‘safe’ area on our page. The downside of this method is that we cannot use actions in our AppBar and also back button and navigation are not done automatically for us (in both cases we would need to create custom ones). This method is simple and works well for example for a landing page, but not so much for a detail page where actions and navigation are very common.
  • Make AppBar transparent. This method requires a bit more complex widget tree but is ideal if we want to keep our AppBar and the behavior that comes with it.

I’m going to explain both methods using a fullscreen background image.

I always use Unsplash for free to use images, because it’s an awesome service. You should definitely give it a try.

Create a full-screen page without AppBar

What we do is that we won’t add an app bar to our Scaffold and make SafeArea the root of our body, to avoid any intrusion by the Operating System. We then remove the bottom and top margin of SafeArea to make it truly fullscreen. Finally, we will use BoxDecoration and NetworkImage to show a fullscreen image.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      // we won't use appBar here
      // appBar: AppBar(
      //   title: Text("Full screen page"),
      //   backgroundColor: Colors.purple,
      // ),
      // might need to set this flag, 
      //resizeToAvoidBottomInset: false,
      body: SafeArea(
        bottom: false,
        top: false,
        child: Container(
          decoration: BoxDecoration(
            image: DecorationImage(
              image: NetworkImage('https://images.unsplash.com/photo-1517030330234-94c4fb948ebc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1275&q=80'),
              fit: BoxFit.cover,
            ),
          ),
          child: Center(
            child: Text('mrflutter.com',
              style: TextStyle(
                color: Colors.white,
                fontSize: 30,
              ),
            ),
          ),
        ),
      ),
    );
  }

This is how it will look on the device:

Fullscreen page with background image

Create a full-screen page with transparent AppBar

With this method, we will make AppBar part of the body and use Stack and Positioned to put AppBar on top of the rest of the body. This way we can actually make the color of our AppBar transparent, as there’s a widget “under” it. Also, to make it look flat, we remove the elevation from the AppBar.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: <Widget>[
          Container(
            decoration: BoxDecoration(
              image: DecorationImage(
                image: NetworkImage(
                    'https://images.unsplash.com/photo-1517030330234-94c4fb948ebc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1275&q=80'),
                fit: BoxFit.cover,
              ),
            ),
            child: Center(
              child: Text(
                'mrflutter.com',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 30,
                ),
              ),
            ),
          ),
          Positioned(
            child: AppBar(
              title: Text("Transparent AppBar"),
              backgroundColor: Colors.transparent,
              elevation: 0,
              actions: <Widget>[
                IconButton(
                  icon: Icon(Icons.share),
                  onPressed: () {},
                  tooltip: 'Share',
                ),
              ],
            ),
          )
        ],
      ),
    );
  }

And this is how it will look like:

Fullscreen page with transparent AppBar

As far as I know, these are the 2 way nicest ways of achieving fullscreen pages with or without transparent AppBar. You can see which method suits your needs better.

FloatingActionButton deep dive Part 2: Create a Speed Dial widget

Share this post

In my previous post, I went through 2 types of FloatingActionButtons and explained how to position them on a page. As promised in this post I’m going to show you how to create a so-called Speed Dial widget using FABs and animation. It’s actually quite easy to do. I’m going to show you step-by-step how to achieve this.

To be able to re-use our Speed Dial widget and don’t pollute our page we should create a StatefulWidget. I have explained Stateless and StatefulWidgets in one of my previous posts. But to recap, we need a StatefulWidget, because we need to change our state. In this case, we need to show and hide FABs and keep track of our animation.

Our StatefulWidget needs to use SingleTickerProviderStateMixin to be notified about animation frames.

class AnimatedFab extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => AnimatedFabState();
}

class AnimatedFabState extends State<AnimatedFab>
    with SingleTickerProviderStateMixin {

  @override
  void initState() {
    // We will initialize our animation here
    super.initState();
  }

  @override
  void dispose() {
    // Here we will dispose of our AnimationController
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // This is where our FABs will be rendered
    return null;
  }

}

Now that we have our StatefulWidget ready, we will start defining our animation and our FABs. We need the following things to setup:

  • AnimationController: as the name says, to control our animation
  • Animation<Color>: this is needed if we want to animate the color of the FAB too
  • Animation<double>: this is the progress needed to animate the icon. This is useful to animate icon and change it when our Speed Dial menu is open.
  • Curve: we need this class to define how our animation changes over time
  • isOpened: a flag to keep track of whether or not our menu is open
class AnimatedFab extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => AnimatedFabState();
}

class AnimatedFabState extends State<AnimatedFab>
    with SingleTickerProviderStateMixin {
  bool isOpened = false;
  AnimationController _animationController;
  Animation<Color> _animateColor;
  Animation<double> _animateIcon;
  Curve _curve = Curves.easeOut;

  @override
  void initState() {
    _animationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 500))
          ..addListener(() {
            setState(() {});
          });
    _animateIcon =
        Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
    _animateColor = ColorTween(
      begin: Colors.blue,
      end: Colors.lightBlue,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Interval(
        0.00,
        1.00,
        curve: _curve,
      ),
    ));
    super.initState();
  }

  void animate() {
    if (!isOpened) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }
    isOpened = !isOpened;
  }

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

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      backgroundColor: _animateColor.value,
      onPressed: animate,
      tooltip: 'Open menu',
      child: AnimatedIcon(
        icon: AnimatedIcons.menu_close,
        progress: _animateIcon,
      ),
    );
  }
}

Notice that AnimatedIcon is a very useful widget that works similar to a normal icon, but shows the animation based on the progress. The list of available animated icons is unfortunately limited. Let’s see how it looks now.

The main FAB is now nicely animated. What we need to do now is to add our menu items (other FABs and according to Google you should not add more than 6 of them, 7 including the menu FAB itself).

We will use translation to open the menu upwards and keep in mind the distance between the icons. So let make some changes to the code.

import 'package:flutter/material.dart';

class SpeedDial extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => SpeedDialState();
}

class SpeedDialState extends State<SpeedDial>
    with SingleTickerProviderStateMixin {
  bool _isOpened = false;
  AnimationController _animationController;
  Animation<Color> _buttonColor;
  Animation<double> _animateIcon;
  Animation<double> _translateButton;
  Curve _curve = Curves.easeOut;
  // this is needed to know how much to "translate"
  double _fabHeight = 56.0;
  // when the menu is closed, we remove elevation to prevent 
  // stacking all elevations
  bool _shouldHaveElevation = false;

  @override
  initState() {
   // a bit faster animation, which looks better: 300
    _animationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 300))
          ..addListener(() {
            setState(() {});
          });
    _animateIcon =
        Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
    _buttonColor = ColorTween(
      begin: Colors.blue,
      end: Colors.red,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Interval(
        0.00,
        1.00,
        curve: Curves.linear,
      ),
    ));
    
   // this does the translation of menu items
    _translateButton = Tween<double>(
      begin: _fabHeight,
      end: -14.0,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Interval(
        0.0,
        0.75,
        curve: _curve,
      ),
    ));
    super.initState();
  }

  void animate() {
    if (!_isOpened) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }
    _isOpened = !_isOpened;
    // here we update whether or not they FABs should have elevation
    _shouldHaveElevation = !_shouldHaveElevation;
  }

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

  Widget composeButton() {
    return Container(
      child: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Compose',
        child: Icon(Icons.email),
        elevation: _shouldHaveElevation ? 6.0 : 0,
      ),
    );
  }

  Widget copyButton() {
    return Container(
      child: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Copy',
        child: Icon(Icons.content_copy),
        elevation: _shouldHaveElevation ? 6.0 : 0,
      ),
    );
  }

  Widget shareButton() {
    return Container(
      child: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Share',
        child: Icon(Icons.share),
        elevation: _shouldHaveElevation ? 6.0 : 0,
      ),
    );
  }

  Widget menuButton() {
    return Container(
      child: FloatingActionButton(
        backgroundColor: _buttonColor.value,
        onPressed: animate,
        tooltip: 'Toggle menu',
        child: AnimatedIcon(
          icon: AnimatedIcons.menu_close,
          progress: _animateIcon,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.end,
      children: <Widget>[
        Transform(
          transform: Matrix4.translationValues(
            0.0,
            _translateButton.value * 3.0,
            0.0,
          ),
          child: composeButton(),
        ),
        Transform(
          transform: Matrix4.translationValues(
            0.0,
            _translateButton.value * 2.0,
            0.0,
          ),
          child: copyButton(),
        ),
        Transform(
          transform: Matrix4.translationValues(
            0.0,
            _translateButton.value,
            0.0,
          ),
          child: shareButton(),
        ),
        menuButton(),
      ],
    );
  }
}

Which looks now like this:

It would be nice to make the widget completely re-usable by adding params to it. For example, it would be nice to be able to define what menu items we want to add to the Speed Dial menu instead of having them hard-coded. It would, however, be a better idea if I explain that later as a separate subject.