03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势

马肤

温馨提示:这篇文章已超过460天没有更新,请注意相关的内容是否还可用!

好的,请您提供需要摘要的内容,我会尽力为您生成一段简洁明了的摘要。

03_Flutter自定义下拉菜单

在Flutter的内置api中,可以使用showMenu实现类似下拉菜单的效果,或者使用PopupMenuButton组件,PopupMenuButton内部也是使用了showMenu这个api,但是使用showMenu时,下拉面板的显示已经被约定死了,只能放一个简单的列表,没有办法定制下来面板的ui,并且下拉面板的宽高需要通过指定constraints进行限制,下面是一个简单的showMenu的用法:

Container(
  height: 44,
  margin: EdgeInsetsDirectional.only(top: 30, start: 30, end: 30),
  color: Colors.red,
  child: Builder(
    builder: (context) {
      return GestureDetector(
        onTap: () {
          final RenderBox button = context.findRenderObject()! as RenderBox;
          final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
          Offset offset = Offset(0.0, button.size.height);
          RelativeRect position = RelativeRect.fromRect(
            Rect.fromPoints(
              button.localToGlobal(offset, ancestor: overlay),
              button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
            ),
            Offset.zero & overlay.size,
          );
          
          showMenu(
            context: context,
            position: position,
            constraints: BoxConstraints(maxWidth: 315, maxHeight: 200),
            items: List.generate(5, (index) => PopupMenuItem(
              child: Container(
                width: 375,
                height: 44,
                alignment: AlignmentDirectional.center,
                child: Text("item"),
              )
            ))
          );
        },
      );
    },
  ),
)

03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第1张

接下来,我们将参照showMenu的源码,依葫芦画个瓢,自定义一个下拉菜单的api,并可自由定制下拉面板的布局内容,篇幅有点长,请耐心观看。

一.确定下拉面板的起始位置

查看PopupMenuButton的源码,可以知道,PopupMenuButton在确定下拉面板的起始位置时,是先获取下拉面板依赖的按钮的边界位置和整个页面的显示区域边界,通过这两个边界计算得到一个RelativeRect,这个RelativeRect就是用来描述下拉面板的起始位置的。

showPopup(BuildContext context) {
  final RenderBox button = context.findRenderObject()! as RenderBox;
  final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
  Offset offset = Offset(0.0, button.size.height);
  RelativeRect position = RelativeRect.fromRect(
    Rect.fromPoints(
      button.localToGlobal(offset, ancestor: overlay),
      button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
    ),
    Offset.zero & overlay.size,
  );
}

注:上述代码中用的的context对象,必须是下拉面板依赖的按钮对应的context,否则最后计算出来的RelativeRect是不对的。计算过程不做过多解释了,直接上图:

03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第2张

二.确定下拉面板的布局约束

  • 水平方向确定最大宽度,比较简单,下拉面板的最大宽度和它所依赖的按钮的宽度一致即可
  • 垂直方向上的最大高度,上一步已经确定了position的值,垂直方向上的最大高度可以取position.top - buttonHeight - padding.top - kToolbarHeight和constraints.biggest.height - position.top - padding.bottom的最大值,padding为安全区域的大小
  • 使用CustomSingleChildLayout作为下拉面板的父容器,并实现一个SingleChildLayoutDelegate,重写getConstraintsForChild,确定约束
    EdgeInsets padding = MediaQuery.paddingOf(context);
    class _CustomPopupRouteLayout extends SingleChildLayoutDelegate {
      final RelativeRect position;
      _CustomPopupRouteLayout(this.position);
      @override
      BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
        Size buttonSize = position.toSize(constraints.biggest);
        double constraintsWidth = buttonSize.width;
        double constraintsHeight = max(position.top - buttonSize.height - padding.top - kToolbarHeight, constraints.biggest.height - position.top - padding.bottom);
        return BoxConstraints.loose(Size(constraintsWidth, constraintsHeight));
      }
      @override
      bool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) {
        return position != oldDelegate.position;
      }
    }
    

    三.显示下拉面板

    我们先把下拉面板显示出来看看效果,这里的下拉面板其实是一个弹出层,而在Flutter中,所有的弹出层的显示和页面路由是一样的,都是通过Navigator.push进行显示,参照showMenu的源码,这里的弹出层我们让其继承PopupRoute

    class _CustomPopupRoute extends PopupRoute {
      final RelativeRect position;
      @override
      final String? barrierLabel;
      _CustomPopupRoute({
        required this.position,
        required this.barrierLabel,
      });
      @override
      Color? get barrierColor => null;
      @override
      bool get barrierDismissible => true;
      @override
      Duration get transitionDuration => Duration(milliseconds: 200);
      @override
      Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
        return CustomSingleChildLayout(
          delegate: _CustomPopupRouteLayout(position),
          child: Material(
            child: Container(
              color: Colors.yellow,
              width: double.infinity,
              height: double.infinity,
              alignment: AlignmentDirectional.center,
              child: Text("popup content"),
            ),
          ),
        );
      }
    }
    
    showPopup(BuildContext context) {
      final RenderBox button = context.findRenderObject()! as RenderBox;
      final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
      Offset offset = Offset(0.0, button.size.height);
      RelativeRect position = RelativeRect.fromRect(
        Rect.fromPoints(
          button.localToGlobal(offset, ancestor: overlay),
          button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
        ),
        Offset.zero & overlay.size,
      );
      
      Navigator.of(context).push(_CustomPopupRoute(
          position: position, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel
      ));
    }
    

    03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第3张

    如图,黄色区域就是下拉面板,可以看到,点击按钮下拉面板显示,点击下拉面板以外的区域,下拉面板关闭,但是位置好像不对,因为我们根本就没去确定下拉面板的位置。

    四.确定下拉面板的位置

    @override
    Offset getPositionForChild(Size size, Size childSize) {
    	return super.getPositionForChild(size, childSize);
    }
    

    只需要重写SingleChildLayoutDelegate的getPositionForChild方法,返回一个Offset对象,Offset的x、y的值就代表下拉面板左上角的位置,那么问题来了,x、y的值怎么确定?

    • 确定x

      x = position.left

    • 确定y

      • position.top + constraintsHeight > size.height - paddingBottom 时

        03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第4张

        • position.top + constraintsHeight final RelativeRect position; EdgeInsets padding; _CustomPopupRouteLayout(this.position, this.padding); @override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { Size buttonSize = position.toSize(constraints.biggest); double constraintsWidth = buttonSize.width; double constraintsHeight = max(position.top - buttonSize.height - padding.top - kToolbarHeight, constraints.biggest.height - position.top - padding.bottom); return BoxConstraints.loose(Size(constraintsWidth, constraintsHeight)); } @override Offset getPositionForChild(Size size, Size childSize) { double x = position.left; double y = position.top; final double buttonHeight = size.height - position.top - position.bottom; double constraintsHeight = max(position.top - buttonHeight - padding.top - kToolbarHeight, size.height - position.top - padding.bottom); if(position.top + constraintsHeight size.height - padding.bottom) { y = position.top - childSize.height - buttonHeight; } return Offset(x, y); } @override bool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) { return position != oldDelegate.position || padding != oldDelegate.padding; } } 03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第5张03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第6张

          六.下拉动画实现

          创建动画插值器,其值从0 ~ 1之间变化,动画时长为PopupRoute中重写的transitionDuration,及200ms时间内,从0变到1,或者从1变到0

          final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));
          

          使用AnimatedBuilder改造PopupRoute的布局结构,根据heightFactorTween的动画执行值 * 下拉菜单内容容器的高度,改变拉菜单内容的高度即可,这里暂时将高度设置为固定值300。

          class _CustomPopupRoute extends PopupRoute {
            
            ...
            
            @override
            Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
              EdgeInsets padding = MediaQuery.paddingOf(context);
              final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));
              return MediaQuery.removePadding(
                context: context,
                removeTop: true,
                removeBottom: true,
                removeLeft: true,
                removeRight: true,
                child: CustomSingleChildLayout(
                  delegate: _CustomPopupRouteLayout(position, padding),
                  child: AnimatedBuilder(
                    animation: animation,
                    builder: (context, child) {
                      return Material(
                        child: Container(
                          height: 300*heightFactorTween.evaluate(animation),
                          child: child,
                        )
                      );
                    },
                    child: Container(
                      color: Colors.yellow,
                      width: double.infinity,
                      height: 300,
                      alignment: AlignmentDirectional.center,
                      child: Text("popup content"),
                    ),
                  ),
                ),
              );
            }
          }
          
          03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第7张03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第8张

          下拉动画效果已经出来了,但是实际情况下,下拉面板的高度是不能直接在组件层固定写死的,所以这里需要动态计算出下拉面板的高度。

          七.下拉面板动态高度,支持下拉动画

          想要获取组件的高度,需要等到组件的layout完成后,才能获取到组件的大小,因此,我们需要自定义一个RenderObject,重写其performLayout,在子控件第一次layout完后,获取到子控件的初始高度,子控件的初始化高度结合动画的高度比例系数来最终确定自身的大小。

          class _RenderHeightFactorBox extends RenderShiftedBox {
            double _heightFactor;
            _RenderHeightFactorBox({
              RenderBox? child,
              double? heightFactor,
            }):_heightFactor = heightFactor ?? 1.0, super(child);
            double get heightFactor => _heightFactor;
            set heightFactor(double value) {
              if (_heightFactor == value) {
                return;
              }
              _heightFactor = value;
              markNeedsLayout();
            }
            @override
            void performLayout() {
              final BoxConstraints constraints = this.constraints;
              if (child == null) {
                size = constraints.constrain(Size.zero);
                return;
              }
              child!.layout(constraints, parentUsesSize: true);
              size = constraints.constrain(Size(
                child!.size.width,
                child!.size.height,
              ));
              child!.layout(constraints.copyWith(maxWidth: size.width, maxHeight: size.height * heightFactor), parentUsesSize: true);
              size = constraints.constrain(Size(
                child!.size.width,
                child!.size.height,
              ));
            }
          }
          

          接着定义一个SingleChildRenderObjectWidget,并引用_RenderHeightFactorBox

          class _HeightFactorBox extends SingleChildRenderObjectWidget {
            final double? heightFactor;
            const _HeightFactorBox({
              super.key,
              this.heightFactor,
              super.child,
            });
            @override
            RenderObject createRenderObject(BuildContext context) => _RenderHeightFactorBox(heightFactor: heightFactor);
            @override
            void updateRenderObject(BuildContext context, _RenderHeightFactorBox renderObject) {
              renderObject.heightFactor = heightFactor ?? 1.0;
            }
          }
          

          最后把下拉面板中,执行动画的child使用_HeightFactorBox包裹,并传入heightFactorTween的执行结果即可。

          @override
          Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
            EdgeInsets padding = MediaQuery.paddingOf(context);
            final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));
            return MediaQuery.removePadding(
              context: context,
              removeTop: true,
              removeBottom: true,
              removeLeft: true,
              removeRight: true,
              child: CustomSingleChildLayout(
                delegate: _CustomPopupRouteLayout(position, padding),
                child: AnimatedBuilder(
                  animation: animation,
                  builder: (context, child) {
                    return Material(
                      child: _HeightFactorBox(
                        heightFactor: heightFactorTween.evaluate(animation),
                        child: child,
                      )
                    );
                  },
                  child: Container(
                    color: Colors.yellow,
                    width: double.infinity,
                    height: double.infinity,
                    alignment: AlignmentDirectional.center,
                    child: Text("popup content"),
                  ),
                ),
              ),
            );
          }
          
          03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第9张03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第10张

          八.完整代码

          class TestPage extends StatelessWidget {
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  title: Text("下拉菜单"),
                  backgroundColor: Colors.blue,
                ),
                body: Container(
                  width: 375,
                  child: Column(
                    mainAxisSize: MainAxisSize.max,
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Container(
                        height: 44,
                        margin: const EdgeInsetsDirectional.only(top: 30, start: 30, end: 30),
                        color: Colors.red,
                        child: Builder(
                          builder: (context) {
                            return GestureDetector(
                              onTap: () {
                                showPopup(context: context, builder: (context) {
                                  return Container(
                                    height: 400,
                                    decoration: const BoxDecoration(
                                      color: Colors.yellow
                                    ),
                                    child: SingleChildScrollView(
                                      physics: const ClampingScrollPhysics(),
                                      child: Column(
                                        mainAxisSize: MainAxisSize.max,
                                        mainAxisAlignment: MainAxisAlignment.center,
                                        crossAxisAlignment: CrossAxisAlignment.stretch,
                                        children: List.generate(29, (index) {
                                          int itemIndex = index ~/ 2;
                                          if(index.isEven) {
                                            return Container(
                                              height: 44,
                                              alignment: AlignmentDirectional.center,
                                              child: Text("item$itemIndex"),
                                            );
                                          } else {
                                            return Container(
                                              height: 1,
                                              color: Colors.grey,
                                            );
                                          }
                                        }),
                                      ),
                                    ),
                                  );
                                });
                              },
                            );
                          },
                        ),
                      ),
                    ],
                  ),
                ),
              );
            }
          }
          showPopup({
            required BuildContext context,
            required WidgetBuilder builder,
            double? elevation,
            Color? shadowColor,
            Duration animationDuration = const Duration(milliseconds: 200)
          }) {
            final RenderBox button = context.findRenderObject()! as RenderBox;
            final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
            Offset offset = Offset(0.0, button.size.height);
            RelativeRect position = RelativeRect.fromRect(
              Rect.fromPoints(
                button.localToGlobal(offset, ancestor: overlay),
                button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
              ),
              Offset.zero & overlay.size,
            );
            Navigator.of(context).push(_CustomPopupRoute(
                position: position,
                builder: builder,
                elevation: elevation,
                shadowColor: shadowColor,
                animationDuration: animationDuration,
                barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel
            ));
          }
          class _CustomPopupRoute extends PopupRoute {
            final WidgetBuilder builder;
            final RelativeRect position;
            final double? elevation;
            final Color? shadowColor;
            @override
            final String? barrierLabel;
            final Duration animationDuration;
            _CustomPopupRoute({
              required this.builder,
              required this.position,
              required this.barrierLabel,
              this.elevation,
              this.shadowColor,
              Duration? animationDuration
            }): animationDuration = animationDuration ?? const Duration(milliseconds: 200),
            super(traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop);
            @override
            Color? get barrierColor => null;
            @override
            bool get barrierDismissible => true;
            @override
            Duration get transitionDuration => animationDuration;
            @override
            Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
              EdgeInsets padding = MediaQuery.paddingOf(context);
              final CurveTween heightFactorTween = CurveTween(curve: const Interval(0.0, 1.0));
              return MediaQuery.removePadding(
                context: context,
                removeTop: true,
                removeBottom: true,
                removeLeft: true,
                removeRight: true,
                child: CustomSingleChildLayout(
                  delegate: _CustomPopupRouteLayout(position, padding),
                  child: AnimatedBuilder(
                    animation: animation,
                    builder: (context, child) {
                      return Material(
                        child: _HeightFactorBox(
                          heightFactor: heightFactorTween.evaluate(animation),
                          child: child,
                        )
                      );
                    },
                    child: builder(context),
                  ),
                ),
              );
            }
          }
          class _CustomPopupRouteLayout extends SingleChildLayoutDelegate {
            final RelativeRect position;
            EdgeInsets padding;
            double childHeightMax = 0;
            _CustomPopupRouteLayout(this.position, this.padding);
            @override
            BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
              Size buttonSize = position.toSize(constraints.biggest);
              double constraintsWidth = buttonSize.width;
              double constraintsHeight = max(position.top - buttonSize.height - padding.top - kToolbarHeight, constraints.biggest.height - position.top - padding.bottom);
              return BoxConstraints.loose(Size(constraintsWidth, constraintsHeight));
            }
            @override
            Offset getPositionForChild(Size size, Size childSize) {
              double x = position.left;
              double y = position.top;
              final double buttonHeight = size.height - position.top - position.bottom;
              double constraintsHeight = max(position.top - buttonHeight - padding.top - kToolbarHeight, size.height - position.top - padding.bottom);
              if(position.top + constraintsHeight > size.height - padding.bottom) {
                y = position.top - childSize.height - buttonHeight;
              }
              return Offset(x, y);
            }
            @override
            bool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) {
              return position != oldDelegate.position || padding != oldDelegate.padding;
            }
          }
          class _RenderHeightFactorBox extends RenderShiftedBox {
            double _heightFactor;
            _RenderHeightFactorBox({
              RenderBox? child,
              double? heightFactor,
            }):_heightFactor = heightFactor ?? 1.0, super(child);
            double get heightFactor => _heightFactor;
            set heightFactor(double value) {
              if (_heightFactor == value) {
                return;
              }
              _heightFactor = value;
              markNeedsLayout();
            }
            @override
            void performLayout() {
              final BoxConstraints constraints = this.constraints;
              if (child == null) {
                size = constraints.constrain(Size.zero);
                return;
              }
              child!.layout(constraints, parentUsesSize: true);
              size = constraints.constrain(Size(
                child!.size.width,
                child!.size.height,
              ));
              child!.layout(constraints.copyWith(maxWidth: size.width, maxHeight: size.height * heightFactor), parentUsesSize: true);
              size = constraints.constrain(Size(
                child!.size.width,
                child!.size.height,
              ));
            }
          }
          class _HeightFactorBox extends SingleChildRenderObjectWidget {
            final double? heightFactor;
            const _HeightFactorBox({
              super.key,
              this.heightFactor,
              super.child,
            });
            @override
            RenderObject createRenderObject(BuildContext context) => _RenderHeightFactorBox(heightFactor: heightFactor);
            @override
            void updateRenderObject(BuildContext context, _RenderHeightFactorBox renderObject) {
              renderObject.heightFactor = heightFactor ?? 1.0;
            }
          }
          
          03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第11张03,(由于您没有提供具体内容,我会假设一个情境生成一个标题,例如,科技发展趋势),科技前沿,揭秘未来发展趋势 第12张

0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

    还没有评论,来说两句吧...

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码