当前位置:   article > 正文

Flutter 使用 Overlay 实现全局弹窗_flutter overlay

flutter overlay

一、Overlay 介绍

1、Overlay 是什么

在官网上关于 Overlay 的介绍是:

简单翻译一下:

Overlay 这个 widget 是一个 基于 Stack 管理的 Widget,可以独立的使用。

Overlay 通过把独立的 widget 插入到 overlay 的 stack 里面来实现让这个 widget 显示到其他 widget 的上面。Overlay 是通过 OverlayEntry 来管理 widget 的显示层次的(不能直接使用 widget)。

可以直接创建一个 Overlay 对象来使用,但是更一般的,都是使用 WidgetsApp 或者 MaterialApp 里面的 Navigator 创建了 Overlay,Navigator 使用这个 overlay 来管理路由显示效果(我们可以通过这个 overlay 来实现全局弹窗)。

2、Overlay 使用步骤

  • 创建 OverlayEntry
  1. Overlay entry=new OverlayEntry(
  2. builder:(){
  3. //自己的 widget
  4. });
  5. 复制代码
  • 将 OverlayEntry 插入到 Overlay 中
  1. Overlay.of(context).insert(overlayEntry);
  2. 复制代码
  • 将 OverlayEntry 移除
  1. entry.remove();
  2. 复制代码

二、基于 Overlay 实现 Toast

  1. //通过 Overlay 实现 Toast
  2. class Toast {
  3. static void show({@required BuildContext context, @required String message}) {
  4. //1、创建 overlayEntry
  5. OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
  6. return new Positioned(
  7. top: MediaQuery.of(context).size.height * 0.8,
  8. child: new Material(
  9. child: new Container(
  10. width: MediaQuery.of(context).size.width,
  11. alignment: Alignment.center,
  12. child: new Center(
  13. child: new Card(
  14. child: new Padding(
  15. padding: EdgeInsets.all(8),
  16. child: new Text(message),
  17. ),
  18. color: Colors.grey.withOpacity(0.6),
  19. ),
  20. ),
  21. ),
  22. ));
  23. });
  24. //插入到 Overlay中显示 OverlayEntry
  25. Overlay.of(context).insert(overlayEntry);
  26. //延时两秒,移除 OverlayEntry
  27. new Future.delayed(Duration(seconds: 2)).then((value) {
  28. overlayEntry.remove();
  29. }
  30. );
  31. }
  32. 复制代码

使用:

  1. RaisedButton(
  2. child: Text('SUBMIT'),
  3. onPressed: () {
  4. Toast.show(context: context, message: "信息已经提交!");
  5. },
  6. )
  7. 复制代码

三、基于 Overlay 实现文字输入框自动提示(补全)

假设我们有一个信息输入界面,如下:

我们希望用户在输入框输入的时候,可以在下方根据用户的输入,当输入框获取焦点时,来展示一个 list 来辅助用户输入(这个可以根据自己的需求来调整),如下:

当然为了实现这个功能,完全可以重写界面,通过 Stack + Position 来完成,但是这样可能需要重构代码,增加界面复杂度,而且可扩展性差,可以通过 Overlay 的方式来实现这个功能。

1、通过 Overlay 来显示悬浮窗

我们需要实现的是,当 TextFormField 获取焦点的时候,就弹出弹窗,当失去焦点的时候,就移除这个弹窗。定义这个弹窗控件如下:

  1. class CountriesField extends StatefulWidget {
  2. @override
  3. _CountriesFieldState createState() => _CountriesFieldState();
  4. }
  5. class _CountriesFieldState extends State<CountriesField> {
  6. final FocusNode _focusNode = FocusNode();
  7. OverlayEntry _overlayEntry;
  8. @override
  9. void initState() {
  10. _focusNode.addListener(() {
  11. if (_focusNode.hasFocus) {
  12. this._overlayEntry = this._createOverlayEntry();
  13. Overlay.of(context).insert(this._overlayEntry);
  14. } else {
  15. this._overlayEntry.remove();
  16. }
  17. });
  18. }
  19. OverlayEntry _createOverlayEntry() {
  20. RenderBox renderBox = context.findRenderObject();
  21. var size = renderBox.size;
  22. var offset = renderBox.localToGlobal(Offset.zero);
  23. return OverlayEntry(
  24. builder: (context) => Positioned(
  25. left: offset.dx,
  26. top: offset.dy + size.height + 5.0,
  27. width: size.width,
  28. child: Material(
  29. elevation: 4.0,
  30. child: ListView(
  31. padding: EdgeInsets.zero,
  32. shrinkWrap: true,
  33. children: <Widget>[
  34. ListTile(
  35. title: Text('Syria'),
  36. ),
  37. ListTile(
  38. title: Text('Lebanon'),
  39. )
  40. ],
  41. ),
  42. ),
  43. )
  44. );
  45. }
  46. @override
  47. Widget build(BuildContext context) {
  48. return TextFormField(
  49. focusNode: this._focusNode,
  50. decoration: InputDecoration(
  51. labelText: 'Country'
  52. ),
  53. );
  54. }
  55. }
  56. 复制代码
  • 通过 FocusNode 来监听 TextFormField 获取或者失去焦点事件,FocusNode 在 initState 里面进行了初始化。
  • 当 TextFormField 获取焦点时,我们把通过 _createOverlayEntry 创建的 OverlayEntry 用 Overlay.of(context).insert 方法 插入到 Overlay 的最上面,这样就显示了这个悬浮窗。
  • 当 TextFormField 失去焦点的时候,我们通过 _overlayEntry.remove 方法来移除弹窗。
  • 因为我们显示弹窗的时候,需要知道这个弹窗的显示的位置,这个位置肯定是和当前输入框相关的,我们可以通过 context.findRenderObject 方法来找到当前 RenderBox,根据这个 RenderBox 我们就可以知道当前显示的这个 widget 的位置,大小或者其他渲染信息,根据这些信息我我们就可以调整弹窗的位置和大小。
  • renderBox.localToGlobal(Offset.zero) 表示获取当前 widget 在屏幕中的位置。
  • 弹窗内部,我们通过 Positioned 来定位弹窗,通过 ListView 来展示内容。

但是,当使用这个自定义控件时,会出现一个问题,就是当 ListView 足够长,我们滚动 ListView 时,这个弹窗的位置是固定不变的。

2、弹窗跟随滚动

我们需要做到是,我们的全局弹窗可以跟随 TextField 滚动,Flutter 提供了两个组件:CompositedTransformFollower 和 CompositedTransformTarget 。

我们将一个 follower 和 一个 target 链接在一起(为这两个 widget 指定相同的 LayerLink),这样当 target 滚动的时候,follower 就会跟随滚动。

在我们这个示例里,弹窗相当于 Follower,而 TextField 是 Target 。我们用 CompositedTransformFollower 和 CompositedTransformTarget 分别包裹弹窗和 TextField,代码如下:

  1. class CountriesField extends StatefulWidget {
  2. @override
  3. _CountriesFieldState createState() => _CountriesFieldState();
  4. }
  5. class _CountriesFieldState extends State<CountriesField> {
  6. final FocusNode _focusNode = FocusNode();
  7. OverlayEntry _overlayEntry;
  8. final LayerLink _layerLink = LayerLink();
  9. @override
  10. void initState() {
  11. _focusNode.addListener(() {
  12. if (_focusNode.hasFocus) {
  13. this._overlayEntry = this._createOverlayEntry();
  14. Overlay.of(context).insert(this._overlayEntry);
  15. } else {
  16. this._overlayEntry.remove();
  17. }
  18. });
  19. }
  20. OverlayEntry _createOverlayEntry() {
  21. RenderBox renderBox = context.findRenderObject();
  22. var size = renderBox.size;
  23. return OverlayEntry(
  24. builder: (context) => Positioned(
  25. width: size.width,
  26. child: CompositedTransformFollower(
  27. link: this._layerLink,
  28. showWhenUnlinked: false,
  29. offset: Offset(0.0, size.height + 5.0),
  30. child: Material(
  31. elevation: 4.0,
  32. child: ListView(
  33. padding: EdgeInsets.zero,
  34. shrinkWrap: true,
  35. children: <Widget>[
  36. ListTile(
  37. title: Text('Syria'),
  38. onTap: () {
  39. print('Syria Tapped');
  40. },
  41. ),
  42. ListTile(
  43. title: Text('Lebanon'),
  44. onTap: () {
  45. print('Lebanon Tapped');
  46. },
  47. )
  48. ],
  49. ),
  50. ),
  51. ),
  52. )
  53. );
  54. }
  55. @override
  56. Widget build(BuildContext context) {
  57. return CompositedTransformTarget(
  58. link: this._layerLink,
  59. child: TextFormField(
  60. focusNode: this._focusNode,
  61. decoration: InputDecoration(
  62. labelText: 'Country'
  63. ),
  64. ),
  65. );
  66. }
  67. }
  68. 复制代码
  • 在上面的代码里,我们通过 CompositedTransformFollower 包裹了 OverlayEntry,通过 CompositedTransformTarget 包裹了 TextFormTield 。
  • 我们为 Follower 和 Target 提供了相同的 LayerLink 实例,这样可以实现弹窗跟随滚动的效果。
  • 因为弹窗跟随 target 滚动,因此在 Positioned 里面不在需要 top 和 left 属性了。
  • 设置 showWhenUnlinked 为 false,这样当target 不在屏幕中时(滚出屏幕),设置弹窗隐藏。

效果 :

参考: Using Overlay to display floating widgets

代码: github


欢迎关注「Flutter 编程开发」微信公众号 。
作者:Flutter编程开发
链接:https://juejin.cn/post/6844904115764658189
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号