I am trying to show an overlay of the same width as of the TextField. The below code sample works fine but when I run it in the browser and resize the window, The Textfield resizes but the overlay stays with the old dimensions.
How can we update (not rerender the overlay) the dimensions of the overlay on resizing the window? I know flutter has a built in AutoComplete widget but it is buggy since we cannot add padding around the overlay as of today. So I am trying to build my own widget.
Here's a sample code I have tried
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final ScrollController _scrollController = ScrollController();
Widget _list() {
return Container(
height: 5 * 40,
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true,
child: ListView.builder(
controller: _scrollController,
padding: EdgeInsets.zero,
itemCount: 20,
itemBuilder: (context, index) => ListTile(
title: Text('item $index'),
onTap: () {
_overlayEntry.remove();
},
))),
);
}
final LayerLink _layerLink = LayerLink();
OverlayEntry _createOverlay() {
final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) => Positioned(
left: offset.dx,
width: size.width,
child: CompositedTransformFollower(
offset: Offset(0, 50),
link: _layerLink,
child: Material(color: Colors.red, child: _list())),
));
}
late OverlayEntry _overlayEntry;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
_overlayEntry = _createOverlay();
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CompositedTransformTarget(
link: _layerLink,
child: TextFormField(
decoration: InputDecoration(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.black.withOpacity(0.8),
),
),
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
),
),
onTap: () {
if (_overlayEntry.mounted) {
_overlayEntry.remove();
}
Overlay.of(context).insert(_overlayEntry);
},
onChanged: (query) {},
),
),
],
),
),
);
}
}
Heres the output of the above code
Notice that Overlay dimensions do not match the window size (when window was resized)
We could force the overlay dimensions by adjusting the Positioned.right
parameter
OverlayEntry(
builder: (context) => Positioned(
left: 0,
right: 0, // custom offset from right
// width: textFieldSize.width,
child: CompositedTransformFollower(
offset: Offset(0, 50),
link: _layerLink,
child: Material(color: Colors.red, child: _list())),
));
But this would fail if we were to give padding around the Textfield
e.g if I set Positioned.right:0
and Give Padding around Textfield the overlay would always take up the whole width.
The final solution I tried is to set the width of the textfield to overlay width, But I am not sure how do we update the Textfield renderBox so that the overlay takes up the expected dimensions.
OverlayEntry _createOverlay() {
/// textfield dimensions
final textFieldRenderBox =
textFieldKey.currentContext!.findRenderObject() as RenderBox;
final textFieldOffset = textFieldRenderBox.localToGlobal(Offset.zero);
final textFieldSize = textFieldRenderBox.size;
print('$textFieldSize $textFieldOffset ');
return OverlayEntry(
builder: (context) => Positioned(
left: 0,
width: textFieldSize.width,
child: CompositedTransformFollower(
offset: Offset(0, 50),
link: _layerLink,
child: Material(color: Colors.red, child: _list())),
));
}
Edit: I tried using didChangeMetrics to listen to MediaQueyChanges and update the Overlay using ValueListenableBuilder but its janky and updates with a delay heres the output and the code
We can update the overlay entry by calling
_overlayEntry!.markNeedsBuild();
Alternatively, We could solve this using OverLayPortal a new widget introduced in flutter 3.10.
Originally answered here https://github.com/flutter/flutter/issues/78746#issuecomment-1564893844
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(
title: "My Home Page",
));
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.only(left:24.0,right:12.0),
child: CustomWidget(),
),
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class CustomWidget extends StatefulWidget {
const CustomWidget({Key? key}) : super(key: key);
@override
State<CustomWidget> createState() => _CustomWidgetState();
}
class _CustomWidgetState extends State<CustomWidget> {
final ScrollController _scrollController = ScrollController();
Widget _list() {
return Container(
height: 5 * 40,
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true,
child: ListView.builder(
controller: _scrollController,
padding: EdgeInsets.zero,
itemCount: 20,
itemBuilder: (context, index) => ListTile(
title: Text('item $index'),
onTap: () {
_portalController.hide();
},
))),
);
}
@override
void initState() {
super.initState();
}
GlobalKey gkey = GlobalKey();
final OverlayPortalController _portalController = OverlayPortalController();
@override
Widget build(BuildContext context) {
Widget textfield() => TextFormField(
key: gkey,
decoration: InputDecoration(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.black.withOpacity(0.8),
),
),
border: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
),
),
onTap: () {
if (!_portalController.isShowing) {
_portalController.show();
}
},
onChanged: (query) {},
);
return OverlayPortal(
controller: _portalController,
overlayChildBuilder: (BuildContext context) {
final renderBox = gkey.currentContext!.findRenderObject() as RenderBox;
final Size tSize = renderBox.size;
final Offset offset = renderBox.localToGlobal(Offset.zero);
return Positioned(
left: offset.dx,
top: offset.dy + tSize.height, // + textfield height
child: Material(
color: Colors.red,
child: SizedBox(
width: tSize.width,
child: _list(),
),
),
);
},
child: textfield(),
);
}
}