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;

相关推荐
m0_7482309420 分钟前
Redis 通用命令
前端·redis·bootstrap
YaHuiLiang1 小时前
一切的根本都是前端“娱乐圈化”
前端·javascript·代码规范
ObjectX前端实验室2 小时前
个人网站开发记录-引流公众号 & 谷歌分析 & 谷歌广告 & GTM
前端·程序员·开源
CL_IN2 小时前
企业数据集成:实现高效调拨出库自动化
java·前端·自动化
浪九天3 小时前
Vue 不同大版本与 Node.js 版本匹配的详细参数
前端·vue.js·node.js
qianmoQ4 小时前
第五章:工程化实践 - 第三节 - Tailwind CSS 大型项目最佳实践
前端·css
椰果uu4 小时前
前端八股万文总结——JS+ES6
前端·javascript·es6
微wx笑5 小时前
chrome扩展程序如何实现国际化
前端·chrome
~废弃回忆 �༄5 小时前
CSS中伪类选择器
前端·javascript·css·css中伪类选择器
CUIYD_19895 小时前
Chrome 浏览器(版本号49之后)‌解决跨域问题
前端·chrome