美文网首页
Flutter OverlayPortal创建自定义下拉菜单(一

Flutter OverlayPortal创建自定义下拉菜单(一

作者: Pino | 来源:发表于2024-05-15 15:27 被阅读0次

    一开始看的掘进的这篇 https://article.juejin.cn/post/7227012644506435642
    使用的时候,问题有点多,修改自定义下拉布局太过于麻烦
    后面看到这篇 https://zhuanlan.zhihu.com/p/660081224 比较通俗易懂
    不过有几个bug
    1.打开下拉布局的时候箭头没有翻转
    2.选择内容后,下拉框没有消失,选择的文本没有显示和高亮
    3.点击空白处,下拉框没有收起
    4.点击返回键,下拉框没有收起
    5.去掉了一些不必要的嵌套部件,去掉不必要的变量和判断
    最终代码 如下
    使用

    Stack(
            children: [
              Container(
                width: double.infinity,
                height: double.infinity,
                decoration: BoxDecoration(
                  color: Colors.black.withOpacity(0.5),
                ),
              ),
              Container(
                color: Colors.white,
                // height: 44,
                child: const DropDownMenu(),
              ),
            ],
          )
    
    import 'package:flutter/material.dart';
    import 'package:flutter_haiguan_pro/utils/color_util.dart';
    
    enum FilterTypes { category, slots, areas }
    
    class CategoryModel {
      int? id;
      String? name;
      String? icon;
      bool? check;
    
      CategoryModel({this.id, this.name, this.icon, this.check});
    
      CategoryModel.fromJson(Map<String, dynamic> json) {
        id = json['ID'];
        name = json['Name'];
        icon = json['Icon'];
        check = json['Check'];
      }
    
      Map<String, dynamic> toJson() {
        final Map<String, dynamic> data = <String, dynamic>{};
        data['ID'] = id;
        data['Name'] = name;
        data['Icon'] = icon;
        data['Check'] = check;
        return data;
      }
    }
    
    class DropDownMenuModel {
      LayerLink layerLink;
      String name;
      FilterTypes type;
      List<CategoryModel> list = [];
    
      String get selectText {
        if (list == null || list.isEmpty) return "";
        try {
          return list.firstWhere((element) => element.id != 0 && element.check == true).name!;
        } catch (e) {}
        return "";
      }
    
      DropDownMenuModel({
        required this.name,
        required this.layerLink,
        required this.type,
        required this.list,
      });
    }
    
    class DropDownMenu extends StatefulWidget {
      const DropDownMenu({super.key});
    
      @override
      State<DropDownMenu> createState() => _DropDownMenuState();
    }
    
    class _DropDownMenuState extends State<DropDownMenu> with SingleTickerProviderStateMixin {
      int _curFilterIndex = -1;
      OverlayEntry? _overlayEntry;
      final GlobalKey _buttonRowKey = GlobalKey();
      final List<DropDownMenuModel> _filterList = [
        DropDownMenuModel(name: '类别', type: FilterTypes.category, list: [], layerLink: LayerLink()),
        DropDownMenuModel(name: '排序', type: FilterTypes.slots, list: [], layerLink: LayerLink()),
        DropDownMenuModel(name: '区域', type: FilterTypes.areas, list: [], layerLink: LayerLink()),
      ];
      late AnimationController _animationController;
      late Animation<double> _animation;
      Color _maskColor = Colors.black26;
    
      @override
      void initState() {
        super.initState();
        init();
    
        _animationController = AnimationController(
          duration: const Duration(milliseconds: 300), // 设置动画持续时间
          vsync: this,
        );
        _animation = Tween<double>(begin: 0, end: 1).animate(_animationController); // 定义动画的值范围
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      init() async {
        // 模拟接口获取数据
        getCateList();
        getSlotList();
      }
    
      void getCateList() {
        setState(() {
          _filterList[0].list = [
            CategoryModel(id: 0, name: '不限', check: false),
            CategoryModel(id: 1, name: '火锅', check: false),
            CategoryModel(id: 2, name: '自助餐', check: false),
            CategoryModel(id: 3, name: '西餐', check: false),
            CategoryModel(id: 4, name: '烤肉', check: false),
            CategoryModel(id: 5, name: '甜品', check: false),
            CategoryModel(id: 6, name: '饮品', check: false),
            CategoryModel(id: 7, name: '蛋糕', check: false),
          ];
        });
      }
    
      void getSlotList() {
        setState(() {
          _filterList[1].list = [
            CategoryModel(id: 0, name: '不限', check: false),
            CategoryModel(id: 1, name: '离我最近', check: false),
            CategoryModel(id: 2, name: '综合排序', check: false),
            CategoryModel(id: 3, name: '价格排序', check: false),
          ];
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return WillPopScope(
            onWillPop: () async {
              hideOverlay();
              return true;
            },
            child: Row(
              key: _buttonRowKey,
              children: List.generate(_filterList.length, (index) {
                DropDownMenuModel item=_filterList[index];
                bool isSelect = item.selectText.isNotEmpty;
                return Expanded(
                  child: CompositedTransformTarget(
                    link: item.layerLink,
                    child: GestureDetector(
                      onTap: () => onTabClick(index),
                      child: SizedBox(
                        height: 30,
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(
                              isSelect ? item.selectText : item.name!,
                              style: TextStyle(fontSize: 14, color: isSelect ? Colors.red : Colors.black87),
                            ),
                            Icon(
                              _curFilterIndex == index
                                  ? Icons.keyboard_arrow_up_rounded
                                  : Icons.keyboard_arrow_down_rounded,
                              size: 20,
                              color: isSelect ? Colors.red : Colors.black26,
                            ),
                          ],
                        ),
                      ),
                    ),
                  ),
                );
              }),
            ));
      }
    
      onTabClick(int index){
        // 点击一样的就关闭
        if (_curFilterIndex == index) {
          hideOverlay();
          return;
        }
        setState(() {
          _curFilterIndex = index;
          _animationController.forward();
          _maskColor = Colors.black26;
        });
        changeOverlay(index: index, reset: true);
      }
    
      void changeOverlay({required int index, bool reset = false}) {
        // TAG: 更新OverlayEntry数据需要清空之后重新构建
        if (reset && _overlayEntry != null) {
          _overlayEntry!.remove();
          _overlayEntry = null;
        }
    
        RenderBox? renderBox;
        if (_buttonRowKey.currentContext != null) {
          renderBox = _buttonRowKey.currentContext?.findRenderObject() as RenderBox;
        }
    
        double left = -(renderBox!.size.width / _filterList.length) * index;
        _overlayEntry = OverlayEntry(
          builder: (context) {
            return CompositedTransformFollower(
              link: _filterList[index].layerLink,
              offset: Offset(left, renderBox!.size.height),
              child: Stack(
                children: [
                  AnimatedBuilder(
                    animation: _animationController,
                    builder: (context, child) {
                      return FadeTransition(
                        opacity: _animation,
                        child: InkWell(
                          onTap: hideOverlay,
                          child: Container(color: _maskColor),
                        ),
                      );
                    },
                  ),
                  MediaQuery.removePadding(
                    context: context,
                    removeTop: true,
                    removeBottom: true,
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        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(
                                  decoration: const BoxDecoration(
                                    color: ColorUtil.primaryBg,
                                    borderRadius: BorderRadius.only(
                                      bottomLeft: Radius.circular(8),
                                      bottomRight: Radius.circular(8),
                                    ),
                                  ),
                                  child: ListView(
                                    shrinkWrap: true,
                                    children: _filterList[index].list.asMap().entries.map((e) {
                                      int itemIndex = e.key;
                                      CategoryModel item = e.value;
                                      return _menuItem(cate: item, index: itemIndex, rootIndex: index);
                                    }).toList(),
                                  ),
                                ),
                              ),
                            );
                          },
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            );
          },
        );
    
        Overlay.of(context).insert(_overlayEntry!);
      }
    
      Widget _menuItem({required int index, required int rootIndex, required CategoryModel cate}) {
        return GestureDetector(
          onTap: () {
            for (CategoryModel item in _filterList[rootIndex].list) {
              item.check = false;
            }
            _filterList[rootIndex].list[index].check = true;
            changeOverlay(index: rootIndex, reset: true);
            hideOverlay();
          },
          child: Container(
            color: Colors.white,
            padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 8),
            height: 40,
            alignment: Alignment.topLeft,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text('${cate.name}',
                    style: TextStyle(
                      fontSize: 14,
                      color: _filterList[rootIndex].list[index].check == true ? ColorUtil.primary : Colors.black87,
                    )),
                if (_filterList[rootIndex].list[index].check == true) Icon(Icons.check, size: 16, color: ColorUtil.primary),
              ],
            ),
          ),
        );
      }
    
      hideOverlay() {
        _overlayEntry?.remove();
        _overlayEntry = null;
        setState(() {
          _curFilterIndex = -1;
          _animationController.reverse();
          _maskColor = Colors.transparent;
        });
        print("_curFilterIndex: ${_curFilterIndex}");
      }
    }
    
    

    相关文章

      网友评论

          本文标题:Flutter OverlayPortal创建自定义下拉菜单(一

          本文链接:https://www.haomeiwen.com/subject/qlnvfjtx.html