英文:
How to update OverlayEntry dimensions on window resize in flutter
问题
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.
如何在调整窗口大小时更新(而不是重新渲染)叠加层的尺寸?我知道Flutter有一个内置的AutoComplete小部件,但由于今天我们无法在叠加层周围添加填充,它存在一些问题。因此,我正在尝试构建自己的小部件。
这是我尝试的示例代码:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
// ...(省略了一些代码)...
class _MyHomePageState extends State<MyHomePage> {
final ScrollController _scrollController = ScrollController();
// ...(省略了一些代码)...
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())),
));
}
// ...(省略了一些代码)...
}
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
英文:
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
答案1
得分: 1
我认为你的使用情况可以通过使用AutoComplete小部件来解决,你试过了吗?
class Autocomplete<T extends Object> extends StatelessWidget {
它会显示类似于以下的内容: 图片链接
英文:
i think that your use case could be resolved by using the AutoComplete Widget, have you tried it yet ?
class Autocomplete<T extends Object> extends StatelessWidget {
will will show you something like this
答案2
得分: 0
We could solve this using OverLayPortal a new widget introduced in flutter 3.10.
原始答案在此处回答: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.headline6,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
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(),
);
}
}
英文:
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(),
);
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论