Let's say you have a search bar in your Flutter app and you want to show somekind of hint, for example what can a user search for. I needed something like this in my Easy Chef app.
When a user gives focus to the search bar for the first time, a hint is shown just below it. Clicking anywhere or typing in the search bar dismisses the hint.
Flutter has a perfect UI element for this, OverlayEntry. The official docs have a good explanation of what an OverlayEntry is. TLDR; your existing app is an Overlay which is simillar to a Stack widget. OverlayEntry can be inserted in this stack, so they float over your existing app content. Great, just what we need. We are positioning the entry just below the search bar, so we'll need its global position. We can get it from RenderBox class. So we'll create a new widget that will represent the search bar.
class SearchBarWithHint extends StatefulWidget {
@override
_SearchBarWithHintState createState() => _SearchBarWithHintState();
}
class _SearchBarWithHintState extends State with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return TextField(
focusNode: searchNode,
controller: searchController,
decoration: InputDecoration(
hintText: 'Search ...',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide())),
);
}
}
Let's add methods for opening/closing the hint and finding the position for overlay entry. RenderBox
has a function localToGlobal
, which will give us the global position of the text field (search bar). When building an overlay entry we'll then use Positioned
widget to put it just below the search bar.
void closeHint() {
overlayEntry?.remove();
overlayEntry = null;
isHintShown = false;
}
void showHint() {
findParent();
overlayEntry = _overlayEntryBuilder();
Overlay.of(context).insert(overlayEntry);
isHintShown = true;
}
findParent() {
RenderBox renderBox = context.findRenderObject();
widgetSize = renderBox.size;
textFieldPosition = renderBox.localToGlobal(Offset.zero);
}
And finally a function that returns the overlay entry. The entry is expanded over the whole screen size, but with a transparent background, that way we can place a GestureDetector
as a root widget to catch a tap anywhere on the screen and dismiss the overlay entry.
OverlayEntry _overlayEntryBuilder() {
return OverlayEntry(
maintainState: true,
builder: (context) {
return GestureDetector(
onTap: () {
if (isHintShown) {
closeHint();
}
},
child: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
color: Colors.transparent,
child: Stack(children: [
Positioned(
top: textFieldPosition.dy + 56,
left: 24,
right: 24,
child: Material(
color: Colors.transparent,
child: Stack(children: [
Align(alignment: Alignment.topCenter,
child: Padding(padding: const EdgeInsets.only(right: 8),
child: ClipPath(clipper: ArrowClipper(),
child: Container(
width: 16,
height: 16,
color: Colors.grey.shade900,
),
)),
),
Align(alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.only(top: 15),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade900, borderRadius: BorderRadius.circular(6)),
child: Text(
'Search for any super hero, just type a name like \'Batman\' ... ',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1.copyWith(color: Colors.white),
),
)))
]),
),
)
])));
},
);
}
Full code example with a demo app is on GitHub.