陕西住房城乡住房建设厅网站合肥瑶海区网站建设费用
web/
2025/10/5 11:50:18/
文章来源:
陕西住房城乡住房建设厅网站,合肥瑶海区网站建设费用,湖北住房城乡建设厅网站,wordpress视频教程 百度云03_Flutter自定义下拉菜单 在Flutter的内置api中#xff0c;可以使用showMenu实现类似下拉菜单的效果#xff0c;或者使用PopupMenuButton组件#xff0c;PopupMenuButton内部也是使用了showMenu这个api#xff0c;但是使用showMenu时#xff0c;下拉面板的显示已经被约定… 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),))));},);},),
)接下来我们将参照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是不对的。计算过程不做过多解释了直接上图 二.确定下拉面板的布局约束
水平方向确定最大宽度比较简单下拉面板的最大宽度和它所依赖的按钮的宽度一致即可垂直方向上的最大高度上一步已经确定了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);overrideBoxConstraints 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));}overridebool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) {return position ! oldDelegate.position;}
}三.显示下拉面板
我们先把下拉面板显示出来看看效果这里的下拉面板其实是一个弹出层而在Flutter中所有的弹出层的显示和页面路由是一样的都是通过Navigator.push进行显示参照showMenu的源码这里的弹出层我们让其继承PopupRoute
class _CustomPopupRouteT extends PopupRouteT {final RelativeRect position;overridefinal String? barrierLabel;_CustomPopupRoute({required this.position,required this.barrierLabel,});overrideColor? get barrierColor null;overridebool get barrierDismissible true;overrideDuration get transitionDuration Duration(milliseconds: 200);overrideWidget buildPage(BuildContext context, Animationdouble animation, Animationdouble 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));
}如图黄色区域就是下拉面板可以看到点击按钮下拉面板显示点击下拉面板以外的区域下拉面板关闭但是位置好像不对因为我们根本就没去确定下拉面板的位置。
四.确定下拉面板的位置
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 时 position.top constraintsHeight size.height - paddingBottom 时
EdgeInsets padding MediaQuery.paddingOf(context);class _CustomPopupRouteLayout extends SingleChildLayoutDelegate {final RelativeRect position;EdgeInsets padding;_CustomPopupRouteLayout(this.position, this.padding);overrideBoxConstraints 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));}overrideOffset 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);}overridebool shouldRelayout(covariant _CustomPopupRouteLayout oldDelegate) {return position ! oldDelegate.position || padding ! oldDelegate.padding;}
}六.下拉动画实现
创建动画插值器其值从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 _CustomPopupRouteT extends PopupRouteT {...overrideWidget buildPage(BuildContext context, Animationdouble animation, Animationdouble 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),),),),);}
}下拉动画效果已经出来了但是实际情况下下拉面板的高度是不能直接在组件层固定写死的所以这里需要动态计算出下拉面板的高度。
七.下拉面板动态高度支持下拉动画
想要获取组件的高度需要等到组件的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();}overridevoid 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,});overrideRenderObject createRenderObject(BuildContext context) _RenderHeightFactorBox(heightFactor: heightFactor);overridevoid updateRenderObject(BuildContext context, _RenderHeightFactorBox renderObject) {renderObject.heightFactor heightFactor ?? 1.0;}
}最后把下拉面板中执行动画的child使用_HeightFactorBox包裹并传入heightFactorTween的执行结果即可。
override
Widget buildPage(BuildContext context, Animationdouble animation, Animationdouble 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),),),),);
}八.完整代码
class TestPage extends StatelessWidget {overrideWidget 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: ListWidget.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 _CustomPopupRouteT extends PopupRouteT {final WidgetBuilder builder;final RelativeRect position;final double? elevation;final Color? shadowColor;overridefinal 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);overrideColor? get barrierColor null;overridebool get barrierDismissible true;overrideDuration get transitionDuration animationDuration;overrideWidget buildPage(BuildContext context, Animationdouble animation, Animationdouble 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);overrideBoxConstraints 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));}overrideOffset 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);}overridebool 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();}overridevoid 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,});overrideRenderObject createRenderObject(BuildContext context) _RenderHeightFactorBox(heightFactor: heightFactor);overridevoid updateRenderObject(BuildContext context, _RenderHeightFactorBox renderObject) {renderObject.heightFactor heightFactor ?? 1.0;}
}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/87349.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!