Original address: medium.com/flutter/imp…

Original author: medium.com/perclasson

Release time: September 2, 2018

Imagine this: you’ve designed your charming spreadsheet.

You send it to your product manager, and he looks at it and says, “So I have to type in the whole country? Can’t you just read my suggestions as I type them in?” And you think, “Yeah, he’s right!” Well, he’s right!” So you decide to implement a “TypeAhead”, an “autocomplete” or whatever you want to call it. A text field that displays suggestions as the user enters. You start working…… You know how to get advice, you know how to do logic, you know everything…… Except how to float suggestions over other widgets.

If you think about it, in order to do this, you would have to redesign the entire screen as a Stack, and then figure out exactly where each widget must be displayed. It’s very cumbersome, very strict, very error-prone, and it just doesn’t feel right. But there is another way.

You can use stacks, or overlays, provided with Flutter.

In this article, I’ll explain how to use overlay Widgets to create widgets that float on top of everything else without having to reorganize your entire view.

You can use it to create autocomplete suggestions, tooltips, or basically anything that floats.

What is an Overlay Widget?

The official documentation defines an Overlay widget as.

A bunch of items that can be managed independently.

Stacking allows individual child widgets to “float” visual elements on top of other widgets by being inserted into the stacked stack.

This is exactly what we’re looking for. When we create the MaterialApp, it automatically creates a Navigator, which in turn creates an Overlay; A Stack widget that Navigator uses to manage the view’s display.

So let’s see how we can use overlays to solve our problem.

Note: This article focuses on displaying the floating widgets, so it won’t go into too much detail about implementing the TypeAhead (auto-complete) field. If you are interested in a well-coded, highly customizable TypeAhead widget, be sure to check out my package, Flutter_TypeAhead.

Initial program

Let’s start with the simple form.

Scaffold(
  body: Padding(
    padding: const EdgeInsets.all(50.0),
    child: Form(
      child: ListView(
        children: <Widget>[
          TextFormField(
            decoration: InputDecoration(
                labelText: 'Address'
            ),
          ),
          SizedBox(height: 16.0,),
          TextFormField(
            decoration: InputDecoration(
                labelText: 'City'
            ),
          ),
          SizedBox(height: 16.0,),
          TextFormField(
            decoration: InputDecoration(
                labelText: 'Address'
            ),
          ),
          SizedBox(height: 16.0,),
          RaisedButton(
            child: Text('SUBMIT'),
            onPressed: () {
              // submit the form},)],),),)Copy the code
  • It is a simple view with three text fields: country, city, and address.

Then we abstracted the country field into our own stateful widget, which we called CountriesField.

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {

  
  @override
  Widget build(BuildContext context) {
    return TextFormField(
      decoration: InputDecoration(
        labelText: 'Country')); }}Copy the code

What we do next is to display a floating list whenever the field receives focus and hide the list whenever focus is lost. You can change this logic depending on your use case. You might want to display it only when the user enters some characters and remove it when the user hits Enter. In all cases, let’s take a look at how to display the floating widget.

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {

  final FocusNode _focusNode = FocusNode();

  OverlayEntry _overlayEntry;

  @override
  void initState() {
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {

        this._overlayEntry = this._createOverlayEntry();
        Overlay.of(context).insert(this._overlayEntry);

      } else {
        this._overlayEntry.remove(); }}); } OverlayEntry _createOverlayEntry() { RenderBox renderBox = context.findRenderObject();var size = renderBox.size;
    var offset = renderBox.localToGlobal(Offset.zero);

    return OverlayEntry(
      builder: (context) => Positioned(
        left: offset.dx,
        top: offset.dy + size.height + 5.0,
        width: size.width,
        child: Material(
          elevation: 4.0,
          child: ListView(
            padding: EdgeInsets.zero,
            shrinkWrap: true,
            children: <Widget>[
              ListTile(
                title: Text('Syria'),
              ),
              ListTile(
                title: Text('Lebanon'), a)],),),)); }@override
  Widget build(BuildContext context) {
    return TextFormField(
        focusNode: this._focusNode,
      decoration: InputDecoration(
        labelText: 'Country')); }}Copy the code
  • We assign a FocusNode to TextFormField and add a listener to it in initState. We will use this listener to detect when a field gains/loses focus.

  • Whenever we receive focus (_focusNode.hasfocus == true), we create an OverlayEntry using _createOverlayEntry, And insert it into the nearest Overlay widget using overlay.of (context).insert.

  • Whenever we lose focus (_focusNode.hasfocus == false), we use _overlayentry.remove to remove the overwrite entry we added.

  • _createOverlayEntry Uses context.findRenderObject to query the render box of our widget. This render box enables us to know the location, size, and other rendering information of the widget. This will help us later know where to place our floating list.

  • _createOverlayEntry uses the renderBox to get the widget’s size. It also uses renderBox. LocalToGlobal to get the widget’s on-screen coordinates. We provide Offset. Zero for the localToGlobal method, which means we get the (0,0) coordinates in the render box and convert them to the corresponding coordinates on the screen.

  • Then we create an OverlayEntry, which is a widget that displays the widgets in the Overlay.

  • The contents of the OverlayEntry is a toy widget. Remember, the widgets can only fit into the Stack, but also remember, an Overlay is really a Stack.

  • So we set the coordinates of the toy widget, we give it the same X position, the same width, the same Y position as the TextField, but in order not to cover the TextField, we move it a little bit towards the bottom.

  • Inside of toy we display a ListView with the offer we want (I hard-coded a few items in this example). Notice that I put everything in a Material Widget. There are two reasons for this: Because overlays do not contain Material widgets by default, many widgets cannot be displayed without a Material ancestor, and the Material Widget provides an elevation attribute that allows us to shadow the widget to make it look as if it is really floating.

That’s it! Our suggestion box is now floating on top of everything else

Bonus: Follow the scroll!

Before we go, let’s try to learn one more thing! If our view were scrollable, we might notice something.

The suggestion box will scroll with us!

The suggestion box will stick to the location on the screen. In some cases that might be what we want, but in this case we don’t want that, we want it to follow our TextField!

The key here is the word “follow”. Flutter provides us with two widgets: CompositedTransformFollower and CompositedTransformTarget. Simply put, if we link a follower to a target, the follower will follow the target wherever it goes! To link a follower and a target, we must provide the same LayerLink for them.

Therefore, we will use CompositedTransformFollower packing our suggestion box, packing in CompositedTransformTarget our TextField. We will then link them by providing them with the same LayerLink. This will make the suggestion box follow the TextField wherever it goes.

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {

  final FocusNode _focusNode = FocusNode();

  OverlayEntry _overlayEntry;

  final LayerLink _layerLink = LayerLink();

  @override
  void initState() {
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {

        this._overlayEntry = this._createOverlayEntry();
        Overlay.of(context).insert(this._overlayEntry);

      } else {
        this._overlayEntry.remove(); }}); } OverlayEntry _createOverlayEntry() { RenderBox renderBox = context.findRenderObject();var size = renderBox.size;

    return OverlayEntry(
      builder: (context) => Positioned(
        width: size.width,
        child: CompositedTransformFollower(
          link: this._layerLink,
          showWhenUnlinked: false,
          offset: Offset(0.0, size.height + 5.0),
          child: Material(
            elevation: 4.0,
            child: ListView(
              padding: EdgeInsets.zero,
              shrinkWrap: true,
              children: <Widget>[
                ListTile(
                  title: Text('Syria'),
                  onTap: () {
                    print('Syria Tapped');
                  },
                ),
                ListTile(
                  title: Text('Lebanon'),
                  onTap: () {
                    print('Lebanon Tapped'); }, ()], (), (), (); }@override
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
      link: this._layerLink,
      child: TextFormField(
          focusNode: this._focusNode,
        decoration: InputDecoration(
          labelText: 'Country'),),); }}Copy the code
  • We are using CompositedTransformFollower OverlayEntry packing our Material widget, packed in CompositedTransformTarget TextFormField.

  • We provide the same LayerLink instance for the follower and target. This causes the follower to have the same coordinate space as the target, allowing it to effectively follow it.

  • We removed the top and left attributes from the toy widget. These attributes are no longer needed because by default, the follower will have the same coordinates as the target. However, we retain the width property of tourists because if you do not bind it, the follower tends to extend indefinitely.

  • We provides an offset for CompositedTransformFollower, to prohibit it covers TextField (as before).

  • Finally, we set showWhenUnlinked to false to hide the OverlayEntry when the TextField is not visible on the screen (such as when we scroll too far to the bottom).

Just like that, our OverlayEntry now follows our TextField!

Important note:CompositedTransformFollowerStill a little buggy; Even whenThe targetWhen it’s no longer visible,followerHidden from the screen,followerIt still responds to the click event. I have sent a question to the Flutter team.

Github.com/flutter/flu…

And will update the post when the issue is resolved.


Overlay is a powerful widget that gives us a convenient Stack to place our floating widgets on. I’ve used it successfully to create flutter_TypeAhead, and I’m sure you can use it for a variety of use cases as well. I hope you found this useful. Let me know what you think


Translation via www.DeepL.com/Translator (free version)