第3章 CHAPTER 3 Flutter UI布局排版 组件核心基础 在Flutter中,应用于页面布局排版方式的组件Widget有线性排列(Column、Row)、层叠排列(Stack)、流式排列(Flow、Wrap)等。 3.1Column与Row实现线性排列 线性布局指页面布局中的控件摆放方式是以线性的方式摆放的,线性排列分为纵向(垂直方向)和横向(水平方向),如图31所示。 图31线性布局排列效果图 3.1.1Column用来实现竖直方向线性排列 Column组件的主要功能是处理垂直方向的布局,Column可以将子组件在竖直方向线性排列,如图32所示,代码如下: //代码清单 3-1 Column的基本使用 //代码路径 lib/code3/code301_Column.dart class Exam220HomePage extends StatelessWidget { const Exam220HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( //页面的头部 appBar: AppBar(title: const Text("标题")), body: SizedBox( //宽度填充屏幕 width: double.infinity, child: Column( children: const [ Text("测试数据一"), Text("测试数据二"), Text("测试数据三"), ], ), ), ); } } 对于Column来讲,在默认情况下,将竖直方向称为主轴方向,将水平方向称为交叉轴方向,Column默认在主轴方向填充,在交叉轴方向包裹,在上述代码中,通过组件SizeBox来设定填充宽度,Column的宽度也会填充SizeBox的宽度。 图32Column排列效果图 将Column在主轴方向设置为包裹,以及将交叉轴对齐方式设置为左对齐,如图33所示,代码如下: Column( //主轴方向包裹 mainAxisSize: MainAxisSize.min, //主轴方向顶部对齐,默认方式 mainAxisAlignment: MainAxisAlignment.center, //交叉轴方向左对齐,默认居中 crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text("测试数据一"), Text("测试数据二"), Text("测试数据三"), ], ) 图33Column左对齐效果图 3.1.2Row用来实现水平方向线性排列 Row组件的主要功能是处理水平方向的布局,如图34所示,通过Row实现子组件的水平线性排列。 图34Row基本使用 代码如下: //代码清单 3-2 Row的基本使用 //代码路径 lib/code3/code302_Row.dart class Exam302HomePage extends StatelessWidget { const Exam302HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( //页面的头部 appBar: AppBar(title: const Text("标题")), body: Row( //主轴方向包裹 //mainAxisSize: MainAxisSize.min, //主轴方向左对齐,默认方式 //mainAxisAlignment: MainAxisAlignment.center, //交叉轴方向顶部对齐,默认居中 //crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text("测试数据一"), Text("测试数据二"), Text("测试数据三"), ], ), ); } } 对于Row,其主轴指的是水平方向,交叉轴指的是竖直方向,在默认情况下,Row在主轴方向上填充,主轴方向通过属性mainAxisAlignment来决定子Widget的对齐方式,配置的是MainAxisAlignment枚举类型,它的取值如表31所示。 表31主轴alignment对齐方式 类别描述 MainAxisAlignment.start沿着主轴方向开始位置对齐 MainAxisAlignment.end沿着主轴方向结束位置对齐 MainAxisAlignment.center沿着主轴方向居中对齐 MainAxisAlignment.spaceBetween沿着主轴方向平分剩余空间 MainAxisAlignment.spaceAround把剩余空间平分成n份,n是子widget的数量,然后把其中一份空间分成两份,放在第1个child的前面和最后一个child的后面 MainAxisAlignment.spaceEvenly把剩余空间平分成n+1份,然后平分所有的空间 交叉轴方向通过属性crossAxisAlignment来决定子Widget的对齐方式,配置的是CrossAxisAlignment枚举类型,它的取值如表32所示。 表32交叉轴alignment对齐方式 类别描述 CrossAxisAlignment.start交叉轴方向开始位置对齐 CrossAxisAlignment.end交叉轴方向结束位置对齐 CrossAxisAlignment.center交叉轴方向居中位置对齐 CrossAxisAlignment.stretch拉伸,使用子Widget填充交叉轴 CrossAxisAlignment.baseline与基线相匹配(不常用) 3.1.3Column与Row中子Widget按比例权重布局 如图35所示,线性布局Row中两个按钮水平排列,默认按钮包裹子文本的大小,结合Expanded组件使用后,第2个Button填充了水平方向剩余的所有空白区域,代码如下: //代码清单 3-3 Row 权重适配 //代码路径 lib/code3/code303_Row.dart class Exam302HomePage extends StatelessWidget { const Exam302HomePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( //页面的头部 appBar: AppBar(title: const Text("标题")), body: Row( //主轴方向居中对齐(对于Row来讲就是水平方向) mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () {}, child: Text("A"), ), //填充 Expanded( child: ElevatedButton( onPressed: () {}, child: Text("B"), ), ), ], ), ); } } 图35Row中权重适配 如果期望Row中的子Widget平均分配宽度,如图36所示,则可以将Row中的子Widget分别使用Expanded来包裹,代码如下: Row( //主轴方向居中对齐(对于Row来讲就是水平方向) mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( flex: 1, child: ElevatedButton( onPressed: () {}, child: Text("A"), ), ), //填充 Expanded( flex: 1, child: ElevatedButton( onPressed: () {}, child: Text("B"), ), ), ], ) 在这里通过Expanded中配置的flex值,来决定当前Expanded的子Widget占用的height,如在这里的3个区域内容中,Expanded分别将flex配置为1,也就是将当前的Column的高度height平均分成3份,然后每个子Widget占用一份height,也就实现了等比分布。同理应用在Column中,便可在竖直方向按比例分布。 3.2非线性布局综合概述 3.2.1Stack用来实现层叠布局 Stack是将子Widget重叠在一起,如图36所示,小点浮在图片上层。 图36Stack的基本使用 图36中的小点可以使用Container结合裁剪组件ClipOval实现,Stack中结合Positioned组合实现子Widget的上、下、左、右、居中排列,代码如下: //代码清单 3-4 Stack 层叠布局 //代码路径 lib/code3/code304_Stack.dart buildStack() { return SizedBox( width: 40, height: 40,//限定大小 child: Stack( alignment: Alignment.center,//子组合居中 children: [ Positioned( width: 33, height: 33,//设定子组件大小 child: Image.asset( "assets/images/warning_icon.png", ), ), Positioned( top: 0,right: 0,//设定子组件右上角对齐 child: ClipOval(//裁剪 child: Container( width: 10, height: 10, color: Colors.red, ), ), ), ], ), ); } 对于alignment属性配置的值,只对没有配置定位或部分定位的子组件起作用,如本实例中的图片没有设置对齐方式,默认使用Stack的alignment属性配置的对齐方式。 3.2.2Wrap用来实现层叠布局 在使用线性布局Row和Colum时,如果子Widget的宽度或者高度超出屏幕范围,则会报溢出错误,通过Wrap来支持流式布局,溢出部分则会自动折行,如图37所示。 图37Row与Wrap排列对比图 Wrap流式布局的代码如下: //代码清单 3-5 Wrap 流式布局 //代码路径 lib/code3/code305_Wrap.dart Widget buildWrap(){ return Wrap( //包裹的子view children: [ Container(color: Colors.blue,width: 200,height: 45,), Container(color: Colors.yellow,width: 200,height: 45,), Container(color: Colors.grey,width: 200,height: 45,), ], direction: Axis.horizontal, //水平排列,默认此方式 spacing: 12,//主轴方向上的两个Widget之间的间距 runSpacing: 10,//行与行之前的间隔 alignment: WrapAlignment.start,//主轴方向的Widget的对齐方式 runAlignment: WrapAlignment.start,//次轴方向上的对齐方式 ); } Wrap的alignment属性用来配置主轴方向上子Widget的对齐方式,如这里配置为水平方向,它的取值如表33所示。 表33Wrap的alignment取值概述 类别描述 WrapAlignment.start子组件沿开始方向对齐 WrapAlignment.end子组件沿结束方向对齐 WrapAlignment.center子组件居中对齐 WrapAlignment.spaceBetween使子组件在主轴方向平均分配未占用的空间,两端对齐 WrapAlignment.spaceAround在主轴方向,把剩余空间平分成n份,n是子widget的数量,然后把其中一份空间分成两份,放在第1个child的前面和最后一个child的后面 WrapAlignment.spaceEvenly在主轴方向,把剩余空间平分成n+1份,然后平分所有的空间 3.2.3实现登录页面 使用层叠布局与线性布局,如图38所示,结合第1章中的基础组件实现登录页面,读者可观看视频【3.2.3登录页面Demo】。 24min 图38登录页面显示效果图 页面主结构是通过层叠布局Stack将背景层、模糊层、信息输入层展示在一起,代码如下: //代码清单 3-6 登录页面Demo //代码路径 lib/code3/code305_Wrap.dart class Exam306HomePage extends StatefulWidget { const Exam306HomePage({Key? key}) : super(key: key); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( body: SizedBox( width: double.infinity, height: double.infinity, child: Stack( children: [ //第一层背景图片 buildFunction1(), //第二层高斯模糊 buildFunction2(), //第三层登录输入层 buildFunction3(), ], ), ), ); } … } 第一层通过Image加载需要显示的图片,第二层通过BackdropFilter结合ImageFilter实现高斯模糊效果,代码如下: buildFunction2() { return Positioned.fill( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), child: Container( color: Colors.white.withOpacity(0.4), ), ), ); } 本实例的完整源码可查看本书配套源码flutter_base_widget中的代码清单36。 3.3弹框用于提示用户信息 弹框又可称为对话框,用于提示用户,如软件应用升级信息弹框、优惠券活动提示、提交修改删除操作给用户的确认弹框等,本节内容概述Material风格与苹果风格的弹框使用、弹框中内容刷新问题。 一个基本Material风格弹框AlertDialog的内容分布区域效果图,如图39所示。 图39Material风格AlertDialog效果图 3.3.1showDialog显示基本弹框 showDialog()方法是Material组件库提供的一个用于弹出Material风格弹框的方法,代码如下: //代码清单 3-7 Dialog 的基本使用 showDialog 运行效果如图3-9所示 //lib/code3/example_307_showDialog.dart void showDialogFunction() async { bool? isSelect = await showDialog( context: context, builder: (context) { return AlertDialog( title: const Text("温馨提示"), //title 的内边距,默认 left: 24.0,top: 24.0, right 24.0 //默认底部边距,如果content不为null,则底部内边距为0 //如果content为null,则底部内边距为20 titlePadding: EdgeInsets.all(10), //标题文本样式 titleTextStyle: TextStyle(color: Colors.black87, fontSize: 16), //中间显示的内容 content: const Text("你确定要删除吗?"), //中间显示的内容边距 //默认 EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0) contentPadding: EdgeInsets.all(10), //中间显示内容的文本样式 contentTextStyle: TextStyle(color: Colors.black54, fontSize: 14), //底部按钮区域 actions: [ TextButton( child: const Text("再考虑一下"), onPressed: () { //关闭返回 false Navigator.of(context).pop(false); }, ), FlatButton( child: const Text("考虑好了"), onPressed: () { //关闭返回值为true Navigator.of(context).pop(true); }, ), ], ); }, ); print("弹框关闭 $isSelect"); } 3.3.2showCupertinoDialog显示苹果风格弹框 showCupertinoDialog用于弹出苹果风格的弹框,如图310所示。 图310showCupertinoDialog苹果风格弹框效果图 代码如下: //代码清单 3-7-1 运行效果如图3-10所示 //lib/code3/example_307_showDialog.dart void showCupertinoDialogFunction(BuildContext context) async { bool isSelect = await showCupertinoDialog( //单击背景弹框是否消失 barrierDismissible: true, context: context, builder: (context) { return CupertinoAlertDialog( title: Text('温馨提示'), //中间显示的内容 content: Text("你确定要删除吗?"), //底部按钮区域 actions: [ CupertinoDialogAction( child: Text('确认'), onPressed: () { Navigator.of(context).pop(); }, ), CupertinoDialogAction( child: Text('取消'), isDestructiveAction: true, onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); } 3.3.3showBottomSheet底部显示弹框 showBottomSheet用来在视图底部弹出一个Material Design风格对话框,如图311所示。这种业务应用场景也比较多,如页面中的分享面板等。 图311showBottomSheet底部弹框效果图 代码如下: //代码清单 3-7-2 showBottomSheet //lib/code3/example_307_showDialog.dart void showBottomSheetFunction(BuildContext context) async { showBottomSheet( context: context, builder: (BuildContext context) { return buildContainer(context); }, ); } Container buildContainer(BuildContext context) { return Container( color: Colors.white, height: 240, width: double.infinity, child: Column( children: [ Container( alignment: Alignment.center, height: 44, child: Text( "温馨提示", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), ), Expanded( child: Text("这里是内容区域"), ), Container( height: 1, color: Colors.grey[200], ), Container( height: 64, child: Row( children: [ Expanded( child: TextButton( child: Text("再考虑一下"), onPressed: () { //关闭后返回值为false Navigator.of(context).pop(false); }, ), ), Container( width: 1, color: Colors.grey[200], ), Expanded( child: FlatButton( child: Text("考虑好了"), onPressed: () { //关闭后返回值为true Navigator.of(context).pop(true); }, ), ), ], ), ), ], ), ); } 在使用showBottomSheet时,可能会出现异常,异常信息如下: [VERBOSE-2:ui_dart_state.cc(177)] Unhandled Exception: No Scaffold widget found. Example701 widgets require a Scaffold widget ancestor. The specific widget that could not find a Scaffold ancestor was: … 这是因为在调用showBottomSheet时使用的Context不是Scaffold对应的Context,可以考虑使用Builder组件来包裹,如在单击按钮时调用显示底部弹框,结合Builder的代码如下: Builder( builder: (BuildContext context) { return ElevatedButton( child: Text("BottomSheet "), onPressed: () { showBottomSheetFunction(context); }, ); }, ), showBottomSheet()方法相当于调用了Scaffold的showBottomSheet()方法,相当于在当前视图中插入显示的一个布局视图。 3.3.4showModalBottomSheet底部弹出对话框 showModalBottomSheet用来在视图底部弹出一个Modal Material Design风格对话框,是菜单或对话框的替代品,可以防止用户与应用程序的其余部分进行交互,showBottomSheet创建的底部弹窗视图,不阻止用户与应用程序交互基本使用,代码如下: //代码清单 3-7-3 showModalBottomSheet //lib/code3/example_307_showDialog.dart void showModalBottomSheetFunction(BuildContext context) async { showModalBottomSheet( context: context, //背景颜色 backgroundColor: Colors.grey, //阴影颜色 barrierColor: Color(0x30000000), //单击背景消失 isDismissible: true, //下滑消失 enableDrag: true, builder: (BuildContext context) { //代码清单 3-7-2 中定义的视图布局 return buildContainer(context); }, ); } showCupertinoModalPopup用来快速构建并弹出iOS风格的底部弹框,代码如下: //代码清单 3-7-4 showCupertinoModalPopup //lib/code3/example_307_showDialog.dart void showCupertinoModalFunction(BuildContext context) async { showCupertinoModalPopup( context: context, builder: (cxt) { CupertinoActionSheet dialog = CupertinoActionSheet( title: Text("温馨提示"), message: Text('请选择分享的平台'), //取消按钮 cancelButton: CupertinoActionSheetAction( onPressed: () {}, child: Text("取消"), ), actions: [ CupertinoActionSheetAction( onPressed: () { Navigator.pop(cxt, 1); }, child: Text('QQ')), CupertinoActionSheetAction( onPressed: () { Navigator.pop(cxt, 2); }, child: Text('微信')), CupertinoActionSheetAction( onPressed: () { Navigator.pop(cxt, 3); }, child: Text('系统分享')), ], ); return dialog; }, ); } 运行效果如图312所示。 图312showModalBottomSheet运行效果图 3.4小结 本章概述了线性布局Column、Row、层叠布局Stack、流式布局Wrap的基本排版Widget,用这些排版Widget再结合第1章中的基础组件综合使用,就可以构建出基本的应用程序,这样便进入了Flutter开发的初级阶段。