Let us assume that you have a widget in your application’s UI, and you want to place another floating widget on top of it. Perhaps that widget needs to be rotated or has additional transformations. How do you display the widget and transform its information to the equivalent UI in the application?
The Overlay in Flutter makes it easy to create visual elements on top of other widgets by adding them to the Overlay’s stack. OverlayEntry is used to insert a widget into the Overlay, then Positioned or AnimatedPositioned is used to determine where it will enter within the Overlay. This is useful when you require an item to appear on top of another widget (similar to the Stack widget) without modifying your entire codebase.
Using an Overlay widget in Flutter might seem a little intuitive but can be challenging at times. First, the overlay entry is inserted using a callback method. And it would be best if you also remembered to remove the Entry
using the reference and not the context of the Overlay.
We will be looking at three different examples where we can use an overlay widget to make the application’s UI more user friendly.
This is a simple sign-up screen that you usually see on any modern-day application. It contains four TextFormFields for a full name, email address, password, and confirmation password.
Since it is a TextFormField, we have validators for each of them. The full name must be more than two characters, it must be a proper email address, the password should be more than six characters, and the confirmed password should match the password field.
When the user clicks on the Submit button, it verifies all the above fields, and if there is an error, the suffix icon’s color changes to red, alerting the user that TextFormField does not match its relevant requirement. When the user clicks on the red icon, an Overlay widget displays for about three seconds, and it disappears.
Visually, an Overlay can be mistaken for an AlertDialog or a Snackbar. An Overlay widget displays similarly, but it gives more power for customization to the developer to program it according to the application’s UI requirement.
Let’s dive into the code and see how an Overlay widget will be displayed when an error occurs and the user clicks on it.
We need to create a stateful widget since we will be running some animations along with the Overlay widget.
We have added four TextEditingController
s for name, email, password, and confirm password:
TextEditingController nameController = TextEditingController(); TextEditingController emailController = TextEditingController(); TextEditingController passwordController = TextEditingController(); TextEditingController confirmPassController = TextEditingController();
Again four Color
variables for name, email, password, and confirm password to switch between red and gray colors:
Color? nameColor, emailColor, passColor, confirmPassColor;
Next, we have an AnimationController
and an Animation
object:
AnimationController? animationController; Animation<double>? animation;
We are overriding initState
and initializing AnimationController
and the Animation
object we just created:
@override void initState() { super.initState(); animationController = AnimationController(vsync: this, duration: const Duration(seconds: 1)); animation = CurveTween(curve: Curves.fastOutSlowIn).animate(animationController!); }
The primary function that we need to create will show the Overlay widget. Therefore, we are creating the _showOverlay
function, which we will call when the user clicks on the suffix icon inside the text field when an error occurs. In the _showOverlay
function, we have declared and initialized OverlayState
and OverlayEntry
objects.
OverlayState
?OverlayState
is the current state of the overlay, which uses OverlayEntry
, Entries
using an insert, or insertAll
functions to insert.
OverlayState
constructordebugIsVisible
: Returns a Boolean value to check whether the given OverlayEntry
is visible or notinsert
: Inserts the given OverlayEntry
inside the overlayinsertAll
: Takes a list of OverlayEntries
and inserts all of them inside the overlayrearrange
: Removes and re-inserts all the entries according to the given order in the list of OverlayEntries
OverlayEntry
?An overlay entry is a place inside an overlay that contains a widget. With the help of a Positioned or AnimatedPositioned widget, Entry
positions itself within the overlay using a Stack
layout.
OverlayEntry
constructorbuilder
: Takes in a builder widgetopaque
: Returns a Boolean value to decide whether the Entry
occludes the entire overlay or notmaintainState
: Takes a Boolean value. It forcefully builds an occluded Entry
below an opaque Entry
if it is true
Below is the code for the _showOverlay
function:
void _showOverlay(BuildContext context, {required String text}) async { OverlayState? overlayState = Overlay.of(context); OverlayEntry overlayEntry; overlayEntry = OverlayEntry(builder: (context) { return Positioned( left: MediaQuery.of(context).size.width * 0.1, top: MediaQuery.of(context).size.height * 0.80, child: ClipRRect( borderRadius: BorderRadius.circular(10), child: Material( child: FadeTransition( opacity: animation!, child: Container( alignment: Alignment.center, color: Colors.grey.shade200, padding: EdgeInsets.all(MediaQuery.of(context).size.height * 0.02), width: MediaQuery.of(context).size.width * 0.8, height: MediaQuery.of(context).size.height * 0.06, child: Text( text, style: const TextStyle(color: Colors.black), ), ), ), ), ), ); }); animationController!.addListener(() { overlayState!.setState(() {}); }); // inserting overlay entry overlayState!.insert(overlayEntry); animationController!.forward(); await Future.delayed(const Duration(seconds: 3)) .whenComplete(() => animationController!.reverse()) // removing overlay entry after stipulated time. .whenComplete(() => overlayEntry.remove()); }
After initializing the overlayEntry
, we return a Positioned
widget inside the builder
method to position the Overlay
widget on the screen. Depending on the application’s design, it can be placed and shown anywhere on the mobile screen.
Next, we added a suffix icon button inside the TextFormField
and called the _showOverlay
function inside it.
The validator
property of TextFormField
has conditions based on how the suffix icon changes from gray to red and vice versa if there is an error:
TextFormField( controller: nameController, keyboardType: TextInputType.name, textInputAction: TextInputAction.next, textCapitalization: TextCapitalization.words, validator: (String? value) { if (value == null || value.trim().isEmpty) { nameColor = Colors.red; } if (value.toString().length <= 2) { nameColor = Colors.red; } else { nameColor = Colors.grey; } return null; }, onSaved: (String? value) { _name = value; }, decoration: kTextInputDecoration.copyWith( labelText: 'Full Name', prefixIcon: const Icon(Icons.person), suffixIcon: IconButton( padding: EdgeInsets.zero, onPressed: () { _showOverlay(context, text: 'Name should have more than 2 characters'); }, icon: Icon(Icons.info, color: nameColor //change icon color according to form validation ))), ),
Lastly, we created a submitForm
method that validates the TextFormField
s and saves the form, and we will call it inside the onPressed
function of the Submit
button:
void _submitForm() { setState(() { _autoValidateMode = AutovalidateMode.always; }); final form = _formKey.currentState; if (form == null || !form.validate()) return; form.save(); Fluttertoast.showToast(msg: _name.toString() + _email.toString()); }
Calling the _submitForm
method inside onPressed
of ElevatedButton
, we get this:
ElevatedButton( onPressed: () { _submitForm(); }, style: ElevatedButton.styleFrom( padding: const EdgeInsets.all(10)), child: const Text( 'Submit', style: TextStyle(fontSize: 20), )),
The full code is available on the GitHub repository.
First, create all the necessary objects and variables that we will use in this example. To start, we have an AnimationController
and an empty List
for animation. Then, we have a list of icons containing three icons and a list of colors that include three different colors associated with the icons:
AnimationController? animationController; List animation = []; List icons = [Icons.home, Icons.settings, Icons.location_city]; List colors = [Colors.green, Colors.blueGrey, Colors.purple]; OverlayEntry? overlayEntry; GlobalKey globalKey = GlobalKey();
Next, we have an OverlayEntry
object, and we have also initialized the GlobalKey
variable that will be attached to the FloatingActionButton
‘s key property.
The main difference between the above and the _showOverlay
function here is the RenderBox
and Offset
widget. The RenderBox
and Offset
widget and the Positioned
widget position the three small icons above the main FloatingActionButton
. When the user clicks the main FloatingActionButton
, the three small icons open upward and close in a downward trend after the stipulated time mentioned in the animation:
_showOverLay() async { RenderBox? renderBox = globalKey.currentContext!.findRenderObject() as RenderBox?; Offset offset = renderBox!.localToGlobal(Offset.zero); OverlayState? overlayState = Overlay.of(context); overlayEntry = OverlayEntry( builder: (context) => Positioned( left: offset.dx, bottom: renderBox.size.height + 16, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ for (int i = 0; i < animation.length; i++) ScaleTransition( scale: animation[i], child: FloatingActionButton( onPressed: () { Fluttertoast.showToast(msg: 'Icon Button Pressed'); }, child: Icon( icons[i], ), backgroundColor: colors[i], mini: true, ), ) ], ), ), ); animationController!.addListener(() { overlayState!.setState(() {}); }); animationController!.forward(); overlayState!.insert(overlayEntry!); await Future.delayed(const Duration(seconds: 5)) .whenComplete(() => animationController!.reverse()) .whenComplete(() => overlayEntry!.remove()); }
In Example 1, we did not attach the Overlay widget with the suffix icon and displayed the widget at our convenient position. But in Example 2, we had to connect to the Overlay widget with FloatingActionButton to serve its primary purpose, giving more options when the user clicks the FAB button:
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Animated Overlay'), ), floatingActionButton: FloatingActionButton( key: globalKey, onPressed: _showOverLay, child: const Icon(Icons.add), ), ); } }
The full code is available on the GitHub repository.
Similar to the previous example, we are attaching the Overlay widget with the TextFormField. It appears when a user clicks on it to enter some text and disappears when one clicks away from it. To be more precise, when TextFormField has focus, the Overlay widget appears with suggestions. When it loses focus or if the user scrolls and the TextFormField is not visible on the screen, the Overlay disappears.
Here we have created a separate stateful widget for the country TextFormField and then called it inside our Profile Page, which is a stateless widget.
FocusNode
to the TextFormField
and added a listener to it inside initState
to detect when the TextFormField
gains or loses focusFocusNode
, we are creating and inserting the OverlayEntry
widgetRenderBox
to know the exact position, size, and other rendering information needed for our widgetRenderBox
to get the widget’s size and the widget’s coordinates on the screenPositioned
widgetPositioned
widget, we display a list of countries inside a Column
widget using a ListTile
. (I have hardcoded a few entries for this example):
final FocusNode _focusNode = FocusNode(); OverlayEntry? _overlayEntry; GlobalKey globalKey = GlobalKey(); final LayerLink _layerLink = LayerLink(); @override void initState() { super.initState(); OverlayState? overlayState = Overlay.of(context); WidgetsBinding.instance!.addPostFrameCallback((_) { globalKey; }); _focusNode.addListener(() { if (_focusNode.hasFocus) { _overlayEntry = _createOverlay(); overlayState!.insert(_overlayEntry!); } else { _overlayEntry!.remove(); } }); }
Now we want our Overlay to follow our TextFormWidget while the user scrolls.
Flutter provides two unique widgets:
Basically, we link the follower to the target, and then the follower will follow the target widget. To do that, we have to provide both widgets with the same LayerLink
.
Material
widget with the CompositedTransformFollower
TextFormField
with CompositedTransformTarget
CompositedTransfromFollower
so that it does not cover the TextFormField
showWhenUnlinked
property to false
to hide the Overlay
when TextFormField
is not visible when the user scrollsAnd now, the OverlayEntry
will follow the TextFormField
:
OverlayEntry _createOverlay() { RenderBox renderBox = context.findRenderObject() as RenderBox; var size = renderBox.size; return OverlayEntry( builder: (context) => Positioned( width: size.width, child: CompositedTransformFollower( link: _layerLink, showWhenUnlinked: false, offset: Offset(0.0, size.height + 5.0), child: Material( elevation: 5.0, child: Column( children: const [ ListTile( title: Text('India'), ), ListTile( title: Text('Australia'), ), ListTile( title: Text('USA'), ), ListTile( title: Text('Canada'), ), ], ), ), ), )); } @override Widget build(BuildContext context) { return CompositedTransformTarget( link: _layerLink, child: TextFormField( focusNode: _focusNode, keyboardType: TextInputType.text, textCapitalization: TextCapitalization.words, textInputAction: TextInputAction.next, decoration: kTextInputDecoration.copyWith(labelText: 'Country Name'), ), ); } }
The Profile Page is a stateless widget, and you can find the full code for it here.
That is all for this tutorial. I hope I was able to impart new knowledge to your existing experience in Flutter development. I know I have learned some great new features about Flutter and its widgets, and I hope to use them in future projects. I usually end with this quote that I love: “Discovery requires experimentation.”
Thank you! Take care and stay safe.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Implementing overlays in Flutter"
after showing overlayEntry and keep it shown so then you interact with any widget clickable or any widget that can interactive , out side the of overlyEntry it the overlyEntry will rebuild , why?, and how can stop it ?