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
Overlay entry=new OverlayEntry(
builder:(){
//自己的 widget
});
复制代码
  • 将 OverlayEntry 插入到 Overlay 中
Overlay.of(context).insert(overlayEntry);
复制代码
  • 将 OverlayEntry 移除
entry.remove();
复制代码

二、基于 Overlay 实现 Toast


//通过 Overlay 实现 Toast
class Toast {
  static void show({@required BuildContext context, @required String message}) {

    //1、创建 overlayEntry
    OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
      return new Positioned(
          top: MediaQuery.of(context).size.height * 0.8,
          child: new Material(
            child: new Container(
              width: MediaQuery.of(context).size.width,
              alignment: Alignment.center,
              child: new Center(
                child: new Card(
                  child: new Padding(
                    padding: EdgeInsets.all(8),
                    child: new Text(message),
                  ),

                  color: Colors.grey.withOpacity(0.6),
                ),
              ),
            ),
          ));
    });


    //插入到 Overlay中显示 OverlayEntry
    Overlay.of(context).insert(overlayEntry);


    //延时两秒,移除 OverlayEntry
    new Future.delayed(Duration(seconds: 2)).then((value) {
      overlayEntry.remove();
    }
    );
  }
复制代码

使用:

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

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

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

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

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

1、通过 Overlay 来显示悬浮窗

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

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {

  final FocusNode _focusNode = FocusNode();

  OverlayEntry _overlayEntry;

  @override
  void initState() {
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {

        this._overlayEntry = this._createOverlayEntry();
        Overlay.of(context).insert(this._overlayEntry);

      } else {
        this._overlayEntry.remove();
      }
    });
  }

  OverlayEntry _createOverlayEntry() {

    RenderBox renderBox = context.findRenderObject();
    var size = renderBox.size;
    var offset = renderBox.localToGlobal(Offset.zero);

    return OverlayEntry(
      builder: (context) => Positioned(
        left: offset.dx,
        top: offset.dy + size.height + 5.0,
        width: size.width,
        child: Material(
          elevation: 4.0,
          child: ListView(
            padding: EdgeInsets.zero,
            shrinkWrap: true,
            children: <Widget>[
              ListTile(
                title: Text('Syria'),
              ),
              ListTile(
                title: Text('Lebanon'),
              )
            ],
          ),
        ),
      )
    );
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
        focusNode: this._focusNode,
      decoration: InputDecoration(
        labelText: 'Country'
      ),
    );
  }
}
复制代码
  • 通过 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,代码如下:

class CountriesField extends StatefulWidget {
  @override
  _CountriesFieldState createState() => _CountriesFieldState();
}

class _CountriesFieldState extends State<CountriesField> {

  final FocusNode _focusNode = FocusNode();

  OverlayEntry _overlayEntry;

  final LayerLink _layerLink = LayerLink();

  @override
  void initState() {
    _focusNode.addListener(() {
      if (_focusNode.hasFocus) {

        this._overlayEntry = this._createOverlayEntry();
        Overlay.of(context).insert(this._overlayEntry);

      } else {
        this._overlayEntry.remove();
      }
    });
  }

  OverlayEntry _createOverlayEntry() {

    RenderBox renderBox = context.findRenderObject();
    var size = renderBox.size;

    return OverlayEntry(
      builder: (context) => Positioned(
        width: size.width,
        child: CompositedTransformFollower(
          link: this._layerLink,
          showWhenUnlinked: false,
          offset: Offset(0.0, size.height + 5.0),
          child: Material(
            elevation: 4.0,
            child: ListView(
              padding: EdgeInsets.zero,
              shrinkWrap: true,
              children: <Widget>[
                ListTile(
                  title: Text('Syria'),
                  onTap: () {
                    print('Syria Tapped');
                  },
                ),
                ListTile(
                  title: Text('Lebanon'),
                  onTap: () {
                    print('Lebanon Tapped');
                  },
                )
              ],
            ),
          ),
        ),
      )
    );
  }

  @override
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
      link: this._layerLink,
      child: TextFormField(
          focusNode: this._focusNode,
        decoration: InputDecoration(
          labelText: 'Country'
        ),
      ),
    );
  }
}
复制代码
  • 在上面的代码里,我们通过 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个简单的 Flutter 代码示例,实现一个自定义的弹窗: ```dart import 'package:flutter/material.dart'; class MyDialog extends StatelessWidget { final String title; final String message; final VoidCallback onConfirm; MyDialog({ @required this.title, @required this.message, @required this.onConfirm, }); @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ Padding( padding: EdgeInsets.all(16), child: Text( title, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 20, ), ), ), Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Text( message, textAlign: TextAlign.center, style: TextStyle(fontSize: 16), ), ), SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ FlatButton( child: Text('取消'), onPressed: () { Navigator.of(context).pop(); }, ), SizedBox(width: 16), RaisedButton( child: Text('确定'), onPressed: onConfirm, ), ], ), ], ), ); } } ``` 在这个示例中,我们定义了一个 `MyDialog` 类,它接收一个标题、一个消息和一个确认回调函数作为参数。在 `build` 方法中,我们使用 `Dialog` 来实现弹窗使用 `Column` 和 `Row` 来组织弹窗中的内容。弹窗中包含一个标题、消息、一个“取消”按钮和一个“确定”按钮。我们使用 `FlatButton` 和 `RaisedButton` 来分别实现这两个按钮。 要使用这个自定义弹窗,只需要在需要的地方创建一个 `MyDialog` 实例,并将它传递给 `showDialog` 方法即可: ```dart showDialog( context: context, builder: (BuildContext context) { return MyDialog( title: '标题', message: '这是一条消息。', onConfirm: () { // 确认回调函数 }, ); }, ); ``` 在这个示例中,我们将 `MyDialog` 实例作为 `builder` 函数的返回值,传递给 `showDialog` 方法。当用户点击弹窗中的“确定”按钮时,传递给 `MyDialog` 的确认回调函数将被调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值