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

效果展示

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

需要实现的功能如下

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

使用OverlayEntry自定义一个浮窗

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

  • 自定义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用来关联CompositedTransformTarget与CompositedTransformFollower

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时调用_overlayEntry的remove方法进行清空。

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组件将CompositedTransformFollower的child进行包裹,并创建一个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;

相关推荐
wangruofeng3 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
wangruofeng3 小时前
给前端开发者的 100 个设计参考网站
前端·设计
LinXunFeng6 小时前
拿竞品 App 一扫,技术栈全暴露了
android·flutter·开源
星栈6 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技6 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569156 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马8 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng9 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
清晨Coding10 小时前
为什么用 React Developer Tools 查看组件树这么乱?
前端·react.js
鶴哥只手遮天10 小时前
《Electron 45.2.3 内核解剖》第 4 章 补丁体系:380 个补丁改了什么
前端·electron