Flutter 中使用 OverlayPortal 创建自定义下拉菜单

效果展示

在写Flutter时发现并没有图中这种效果,所以自己写了一个:

需要实现的功能如下

  • 支持列表展开动画
  • 支持背景fade动画
  • 切换时禁止收起
  • 菜单切换

使用OverlayEntry自定义一个浮窗

因为需要OverlayEntry跟随点击的按钮,所以需要使用CompositedTransformTargetCompositedTransformFollower关联。

  • 自定义Model存放OverlayEntry
dart 复制代码
class FilterDropDownModel {
  final LayerLink layerLink;
  final String name;
  FilterDropDownModel({required this.name, required this.layerLink});
}

// ...
// 初始化菜单
final List<FilterDropDownModel> _filterList = [
    FilterDropDownModel(name: '类别', layerLink: LayerLink()),
    FilterDropDownModel(name: '排序', layerLink: LayerLink()),
    FilterDropDownModel(name: '区域', layerLink: LayerLink()),
];

LayerLink用来关联CompositedTransformTargetCompositedTransformFollower

dart 复制代码
CompositedTransformTarget(
  link: _filterList[index].layerLink,
  child: GestureDetector(
    onTap: () {
      RenderBox? renderBox;
      if (_buttonRowKey.currentContext != null) {
        renderBox = _buttonRowKey.currentContext?.findRenderObject() as RenderBox;
      }

      Overlay.of(context).insert(
        _overlayEntry = OverlayEntry(
          builder: (context) {
            return CompositedTransformFollower(
              link: _filterList[index].layerLink,
              offset: Offset(0, renderBox!.size.height),
              child: Container(
                color: Colors.red,
                child: ListView(
                  shrinkWrap: true,
                  children: [
                    Text('dataasdasdasd2======$index'),
                    const Text('dataasdasdasd3======'),
                    const Text('dataasdasdasd3======'),
                    const Text('dataasdasdasd=qe====='),
                    const Text('dataasdasdasd======'),
                    const Text('dataasdasdasd======'),
                  ],
                ),
              ),
            );
          },
        ),
      );
    },
    
    child: Container(
      color: Colors.amber,
      height: 30,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            _filterList[index].name,
            style: const TextStyle(fontSize: 14, color: AppColors.black85),
          ),
          const Icon(
            Icons.keyboard_arrow_down_rounded,
            size: 20,
            color: Colors.black26,
          ),
        ],
      ),
    ),
  ),
),
  • 由于CompositedTransformFollower跟随的坐标为CompositedTransformTarget的左下角位置

这里计算了Offse的位置,设置CompositedTransformFollower值为offset:Offset(left, renderBox!.size.height)即可解决坐标的问题

dart 复制代码
double left = -(renderBox!.size.width / _filterList.length) * index;

多个tab切换

使用OverlayEntry向页面中添加时,需要先清除之前的OverlayEntry,否则会叠加,当切换tab时调用_overlayEntryremove方法进行清空。

dart 复制代码
_overlayEntry!.remove();
_overlayEntry = null;

菜单高度自适应时的动画

图中红色菜单菜单使用了Container进行包裹并没有设置高度,但如果我们需要使用高度动画,就需要利用Tween动画begin/end的值。

此时可以使用AnimatedBuilder配合SizeTransition进行动画,SizeTransition可以实现高度自适应时的收起展开动画效果:

存储一个_isExpanded变量,用来记录收起展开的状态,该状态用于控制AnimatedBuilder的动画触发时机:

dart 复制代码
_isExpanded = !_isExpanded;

// ...
if (_isExpanded) {
  _animationController.forward();
} else {
  _animationController.reverse();
}

收起展开的效果已经实现,但是收起展开的方向是由中间开始,不是很符合预期,所以这里我们需要稍微改造一下,在SizeTransition内嵌套一个SlideTransition组件并设置position,具体代码如下:

dart 复制代码
AnimatedBuilder(
  animation: _animationController,
  builder: (context, child) {
    return SizeTransition(
      sizeFactor: _animation,
      child: SlideTransition(
        position: Tween<Offset>(
          begin: const Offset(0, -1), // 重点
          end: Offset.zero,
        ).animate(_animation),
        child: Container(
          color: Colors.red,
          child: ListView(
            shrinkWrap: true,
            children: [
              Text('dataasdasdasd2======$index'),
              const Text('dataasdasdasd3======'),
              const Text('dataasdasdasd3======'),
              const Text('dataasdasdasd=qe====='),
              const Text('dataasdasdasd======'),
              const Text('dataasdasdasd======'),
            ],
          ),
        ),
      ),
    );
  },
),

最终的收起展开动画如下图:

添加透明遮照层

到这里其实就比较常规,用Stack组件将CompositedTransformFollowerchild进行包裹,并创建一个AnimatedBuilder作为透明遮照层,

dart 复制代码
Stack(
    children: [
      AnimatedBuilder(
        animation: _animationController,
        builder: (context, child) {
          return FadeTransition(
            opacity: _animation,
            child: Container(color: _maskColor),
          );
        },
      ),
      
      // ...
   ],
),

总结

菜单容器使用OverlayPortal CompositedTransformTarget CompositedTransformFollower三者结合;

收起展开动画使用AnimatedBuilder SizeTransition SlideTransition 三者结合;

渐显的动画直接使用FadeTransition;

相关推荐
阿奇__1 小时前
element 跨页选中,回显el-table选中数据
前端·vue.js·elementui
谢尔登1 小时前
【React】SWR 和 React Query(TanStack Query)
前端·react.js·前端框架
断竿散人1 小时前
专题一、HTML5基础教程-Viewport属性深入理解:移动端网页的魔法钥匙
前端
3Katrina1 小时前
理解Promise:让异步编程更优雅
前端·javascript
星之金币1 小时前
关于我用Cursor优化了一篇文章:30 分钟学会定制属于你的编程语言
前端·javascript
天外来物1 小时前
实战分享:用CI/CD实现持续部署
前端·nginx·docker
moxiaoran57531 小时前
Spring Cloud Gateway 动态路由实现方案
运维·服务器·前端
市民中心的蟋蟀1 小时前
第十一章 这三个全局状态管理库之间的共性与差异 【上】
前端·javascript·react.js
vvilkim1 小时前
Flutter 常用组件详解:Text、Button、Image、ListView 和 GridView
前端·flutter