Flutter 陈航 14-布局控件 Container Row Column Stack 组合与自绘
目录
14 | 经典布局:子控件在父容器中的位置
Flutter 提供了 40+ 种布局 Widget,同一视觉效果可以通过多种布局控件实现。
单子 Widget 布局
单子 Widget 类容器有 15+ 种,一般用来对其唯一的子 Widget 进行样式包装,比如限制大小、添加背景色样式、内间距、旋转变换等。
Container
Container 可以单独作为控件存在,比如单独设置背景色、宽高;也可以作为其他控件的父级存在,但仅能包含一个子 Widget。
对于多个子 Widget 的布局场景,可以先用一个根 Widget 去包装这些子 Widget,然后把这个根 Widget 放到 Container 中。
Container(
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.all(20),
width: 200,
height: 100,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.green, // 背景色
borderRadius: BorderRadius.circular(10), // 圆角边框
),
child: const Text(
'Container',
style: TextStyle(color: Colors.white),
),
);
Padding
如果只需要给子 Widget 设定间距,则可以使用 Padding:
Padding(
padding: EdgeInsets.all(20),
child: Text('Padding', style: TextStyle(color: Colors.green)),
)
通过 EdgeInsets 的不同构造函数,可以分别制定四个方向的不同补白方式。
Center
Center 会将其子 Widget 居中排列。
// 把一个 Text 包在 Center 里,实现居中展示
Center(
child: Text('Center', style: TextStyle(color: Colors.green)),
);
注意:为了实现居中布局,Center 所占据的空间一定要比其子 Widget 大才行。
Container + Center
可实现 Container 容器中的 alignment: Alignment.center
效果。
Container 与 Center 底层都依赖 Align 实现的对齐。
多子 Widget 布局
Row 和 Column
class _HomePageState extends State<HomePage> {
bool _row = false;
List<Widget> list = <Widget>[
Container(color: Colors.deepPurple, width: 50, height: 50),
Container(color: Colors.greenAccent, width: 40, height: 40),
Container(color: Colors.blue, width: 30, height: 30),
Container(color: Colors.green, width: 50, height: 50),
];
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Container(
width: 200,
height: 200,
margin: const EdgeInsets.all(10),
color: Colors.black,
child: _row ? Row(children: list) : Column(children: list),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _row = !_row),
child: Text(_row ? "Row" : "Column"),
));
}
Expanded
单纯使用 Row 和 Column 控件,在子 Widget 的尺寸较小时,无法将容器填满,通过 Expanded 控件,可以制定填满容器剩余空间的分配规则。
List<Widget> list = <Widget>[
Expanded(flex: 1, child: Container(color: Colors.deepPurple, width: 10, height: 10)), // 宽、高 由 Expanded 分配
Container(color: Colors.greenAccent, width: 40, height: 40),
Container(color: Colors.blue, width: 30, height: 30),
Expanded(flex: 1, child: Container(color: Colors.green, width: 10, height: 10)), // 宽、高 即使设置了也不会生效
];
坐标轴对齐规则
Row 与 Column 的主轴
表示容器依次摆放子 Widget 的方向,纵轴
则是与主轴垂直的另一个方向。
mainAxisAlignment
表示子组件在 Row
所占用的水平空间内对齐方式
- 如果
mainAxisSize
的值为MainAxisSize.min
,属性mainAxisAlignment
无意义,因为子组件的宽度等于Row
的宽度 - 只有
mainAxisSize
的值为MainAxisSize.max
,属性mainAxisAlignment
才有意义
Row 主轴方向上的对齐规则 mainAxisAlignment
:
- MainAxisAlignment.start:沿
textDirection
的初始方向对齐,默认左对齐 - MainAxisAlignment.end:默认靠右对齐
- MainAxisAlignment.center:横向居中对齐
- MainAxisAlignment.spaceEvenly:按固定间距对齐
textDirection
表示水平方向子组件的布局顺序,默认为系统当前 Locale 环境的文本方向。中文、英语都是从左往右,而阿拉伯语是从右往左。
Row 纵轴方向上的对齐规则 crossAxisAlignment
:
- CrossAxisAlignment.start:靠上对齐
- CrossAxisAlignment.center:纵向居中对齐
- CrossAxisAlignment.end:靠下对齐
mainAxisSize
Row
的 mainAxisSize
表示在主轴(水平)方向占用的空间:
- MainAxisSize.max:表示尽可能多的占用水平空间,类似 match_parent,此时无论子 widgets 占用多少水平空间,
Row
的宽度始终等于水平方向的最大宽度 - MainAxisSize.min:表示尽可能少的占用水平空间,类似 wrap_content,此时若子 widgets 没有占满水平空间,则 Row 的宽度等于所有子组件占用的的水平空间之和
Row 与 Column 自身的大小由父 widget 的大小
、子 widget 的大小
、mainAxisSize 的设置
三者共同决定的:
- 主轴值为 max:主轴大小等于屏幕主轴方向大小,或者父 widget 在主轴方向大小
- 主轴值为 min:主轴大小等于所有子 widget 组合在一起的大小
层叠 Widget 布局
Stack 与 Android 中的 FrameLayout 非常类似,子 Widget 之间允许叠加。Stack 按照其子 Widget 创建的先后顺序进行层叠摆放。
可以借助 Positioned 控制 Stack 中子 Widget 的摆放位置,还可以根据 Stack 的上、下、左、右四个角的位置来确定子 Widget 的位置。
Stack(
children: <Widget>[
Container(color: Colors.yellow, width: 150, height: 150),
Positioned(left: 10, top: 10, child: Container(color: Colors.green, width: 50, height: 50)),
Positioned(left: 20, top: 40, child: Container(color: Colors.blue, width: 50, height: 50)),
Positioned(right: 10, bottom: 0, child: Container(color: Colors.red, width: 100, height: 100))
],
)
Positioned 控件只能在 Stack 中使用,在其他容器中使用会报错。
15 | 组合与自绘,自定义 Widget
在 Flutter 中自定义 Widget 与其他平台类似,可以使用基本 Widget 组合成一个高级别的 Widget,也可以自己在画板上根据特殊需求来画界面。
组合
布局分析
- 控件间的边距如何设置:使用 Padding 包装
- 中间部分的伸缩、截断规则如何设置:使用 Expanded 包装
- 图片圆角怎么实现:使用 ClipRRect 包装
数据结构
class UpdateItemModel {
String icon; // 图标
String name; // 名称
double size; // 大小
late DateTime dateTime; // 更新日期
String desc; // 更新文案
int version; // 更新版本
UpdateItemModel({this.icon = "", this.name = "", this.size = 0, this.desc = "", this.version = 0}) {
dateTime = DateTime.now();
}
}
上半部分
Widget buildTopRow(UpdateItemModel model) {
Padding leftPadding = Padding(
padding: const EdgeInsets.all(10),
child: ClipRRect(
borderRadius: BorderRadius.circular(8), // 圆角矩形裁剪控件,圆角半径
child: Image.asset(model.icon, width: 80, height: 80),
),
);
Expanded centerExpanded = Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // 垂直方向居中对齐
crossAxisAlignment: CrossAxisAlignment.start, // 水平方向居左对齐
children: <Widget>[
Text(
model.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), // 名字
Text(
"${model.dateTime.year}年${model.dateTime.month}月${model.dateTime.day}日",
maxLines: 1,
style: const TextStyle(color: Colors.grey, fontSize: 12),
), // 更新日期
],
),
);
var border = const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20)));
Padding rightPadding = Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), // 左右边距为 10
child: TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((states) => const Color.fromARGB(10, 0, 0, 0)),
foregroundColor: MaterialStateProperty.resolveWith((states) => Colors.blue),
shape: MaterialStateProperty.resolveWith((states) => border),
textStyle: MaterialStateProperty.resolveWith((states) => const TextStyle(fontWeight: FontWeight.bold)),
minimumSize: MaterialStateProperty.resolveWith((states) => const Size(20, 20)),
fixedSize: MaterialStateProperty.resolveWith((states) => const Size(70, 35)),
),
onPressed: () => log("TextButton onPressed"),
child: const Text("OPEN"),
),
);
return Row(children: <Widget>[leftPadding, centerExpanded, rightPadding]);
}
下半部分
Widget buildBottomRow(UpdateItemModel model) {
return Padding(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), // 左边距和右边距为 15
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // 水平方向距左对齐
children: <Widget>[
Text(
model.desc,
maxLines: 4,
overflow: TextOverflow.ellipsis,
), // 更新文案
Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0), // 上边距为 10
child: Text(
"Version ${model.version} • ${model.size} MB",
style: const TextStyle(color: Colors.grey),
),
)
],
),
);
}
组合控件
class _HomePageState extends State<HomePage> {
UpdateItemModel model = UpdateItemModel(
icon: "assets/images/monkey.jpg",
name: "白乾涛白乾涛白乾涛白乾涛白乾涛",
size: 100.5,
desc: "更新文案\n更新文案\n更新文案更新\n更新文案更新文案更新文案更新文案更新文案更新文案更新文案",
version: 88,
);
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start, // 水平方向居左对齐
children: <Widget>[buildTopRow(model), buildBottomRow(model)],
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => model.version++),
child: Text(model.version.toString()),
));
}
最终效果
自绘
在原生 iOS 和 Android 开发中,可以继承 UIView/View,在 drawRect/onDraw 方法里进行绘制操作。在 Flutter 中也有类似的方案,那就是 CustomPaint。
- Paint:画笔,配置颜色、样式、粗细
- Canvas:画布,提供绘制方法,drawLine/drawRect/drawPoint/drawPath/drawCircle/drawArc
- CustomPainter:绘制逻辑,在 paint 方法里,通过 Canvas 与 Paint 定义要绘制的内容
- CustomPaint:自绘控件,并不负责真正的绘制,通过其 painter 属性关联 CustomPainter
CustomPaint
class _HomePageState extends State<HomePage> {
double size = 100;
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Cake(size: size),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => size += 20),
child: Text(size.toInt().toString()),
));
}
class Cake extends StatefulWidget {
final double size;
const Cake({super.key, required this.size});
@override
State<StatefulWidget> createState() => _CakeState();
}
class _CakeState extends State<Cake> {
@override
Widget build(BuildContext context) => CustomPaint(
size: Size(widget.size, widget.size), // CustomPaint 的宽高
painter: WheelPainter(), // CustomPaint 所关联的 CustomPainter
);
}
CustomPainter
class WheelPainter extends CustomPainter {
Paint getPaint(Color color) => Paint()..color = color;
@override
void paint(Canvas canvas, Size size) {
double radius = min(size.width, size.height) / 2; // 饼图的尺寸
Rect rect = Rect.fromCircle(center: Offset(radius + 10, radius + 10), radius: radius); // 包裹饼图的矩形框
double angle = 2 * pi / 6; // 弧度,分成 6 份,每份 1/6 圆
canvas.drawArc(rect, 0, angle, true, getPaint(Colors.orange));
canvas.drawArc(rect, angle, angle, true, getPaint(Colors.black38));
canvas.drawArc(rect, angle * 2, angle, true, getPaint(Colors.green));
canvas.drawArc(rect, angle * 3, angle, true, getPaint(Colors.red));
canvas.drawArc(rect, angle * 4, angle, true, getPaint(Colors.blue));
canvas.drawArc(rect, angle * 5, angle, true, getPaint(Colors.pink));
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this; // 判断是否需要重绘
}
效果
总结
以组装的方式构建 UI,需要将视图分解成各个 UI 小元素。通常,可以按照从上到下、从左到右的布局顺序去对控件层次结构进行拆解,将基本视觉元素封装到 Column、Row 中;对于有着固定间距的视觉元素,可以通过 Padding 进行包装;对于大小伸缩可变的视觉元素,可以通过 Expanded 控件让其填充父容器的空白区域。
以自绘的方式定义控件,需要借助于 CustomPaint 容器,以及最终承接真实绘制逻辑的 CustomPainter。CustomPainter 是绘制逻辑的封装,在其 paint 方法中,可以使用不同类型的画笔 Paint,借助画布 Canvas 提供的不同类型的绘制图形能力,实现控件自定义绘制。
2023-1-2
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/17020700.html