你是否需要了解 Flutter
布局的案例?
这里我将展示我在使用 Flutter
布局的代码片段。我将通过精美的代码片段结合可视化的图形来举例。
本文注重 Flutter
部件中比较有用的一些来展示,而不是走马观花展示一大推的部件内容。
本文是翻译的文章,采用意译的方式
行(Row)和列(Column)的布局
|
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
|
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
|
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
|
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
|
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
|
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
如果你需要文本是针对基线对齐,那么你应该使用 CrossAxisAlignment.baseline
。
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: <Widget>[
Text(
'Baseline',
style: Theme.of(context).textTheme.headline2,
),
Text(
'Baseline',
style: Theme.of(context).textTheme.bodyText2,
),
],
),
|
Row /*or Column*/(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 200),
Icon(Icons.star, size: 50),
],
),
|
Row /*or Column*/(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 200),
Icon(Icons.star, size: 50),
],
),
|
Row /*or Column*/(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 200),
Icon(Icons.star, size: 50),
],
),
|
Row /*or Column*/(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 200),
Icon(Icons.star, size: 50),
],
),
|
Row /*or Column*/(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
|
Row /*or Column*/(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
在行列布局中,如何使得所有的部件跟宽度/高度最大的部件同宽/同高呢?如下:
我们假有下面的布局:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('IntrinsicWidth')),
body: Center(
child: Column(
children: <Widget>[
RaisedButton(
onPressed: () {},
child: Text('Short'),
),
RaisedButton(
onPressed: () {},
child: Text('A bit Longer'),
),
RaisedButton(
onPressed: () {},
child: Text('The Longest text button'),
),
],
),
),
);
}
那么,你想所有的按钮的宽度都跟最宽的按钮那么宽,那就使用IntrinsicWidth
:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('IntrinsicWidth')),
body: Center(
child: IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
RaisedButton(
onPressed: () {},
child: Text('Short'),
),
RaisedButton(
onPressed: () {},
child: Text('A bit Longer'),
),
RaisedButton(
onPressed: () {},
child: Text('The Longest text button'),
),
],
),
),
),
);
}
同理,如果你想所有的部件的高度跟最高的部件一样高,你需要结合 IntrinsicHeight
和 Row
来实现。
Stack
很适合小部件相互叠加。
Widget build(BuildContext context) {
Widget main = Scaffold(
appBar: AppBar(title: Text('Stack')),
);
return Stack(
fit: StackFit.expand,
children: <Widget>[
main,
Banner(
message: "Top Start",
location: BannerLocation.topStart,
),
Banner(
message: "Top End",
location: BannerLocation.topEnd,
),
Banner(
message: "Bottom Start",
location: BannerLocation.bottomStart,
),
Banner(
message: "Bottom End",
location: BannerLocation.bottomEnd,
),
],
);
}
使用自己的部件,你需要将它们放在Positioned
部件中。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Stack')),
body: Stack(
fit: StackFit.expand,
children: <Widget>[
Material(color: Colors.yellowAccent),
Positioned(
top: 0,
left: 0,
child: Icon(Icons.star, size: 50),
),
Positioned(
top: 340,
left: 250,
child: Icon(Icons.call, size: 50),
),
],
),
);
}
如果你不想猜测顶部/底部的值,你可以使用 LayoutBuilder
部件来检索它们的值。
Widget build(BuildContext context) {
const iconSize = 50;
return Scaffold(
appBar: AppBar(title: Text('Stack with LayoutBuilder')),
body: LayoutBuilder(
builder: (context, constraints) =>
Stack(
fit: StackFit.expand,
children: <Widget>[
Material(color: Colors.yellowAccent),
Positioned(
top: 0,
child: Icon(Icons.star, size: iconSize),
),
Positioned(
top: constraints.maxHeight - iconSize,
left: constraints.maxWidth - iconSize,
child: Icon(Icons.call, size: iconSize),
),
],
),
),
);
}
Expanded
配合 Flex\Flexbox 布局实现,它对于多项目分配空间很棒。
Row(
children: <Widget>[
Expanded(
child: Container(
decoration: const BoxDecoration(color: Colors.red),
),
flex: 3,
),
Expanded(
child: Container(
decoration: const BoxDecoration(color: Colors.green),
),
flex: 2,
),
Expanded(
child: Container(
decoration: const BoxDecoration(color: Colors.blue),
),
flex: 1,
),
],
),
默认的,很多部件多尽量使用小空间,比如:
Card(
child: const Text('Hello World!'),
color: Colors.yellow,
),
ConstrainedBox
允许小部件根据需要使用剩下的空间。
ConstrainedBox(
constraints: BoxConstraints.expand(),
child: const Card(
child: const Text('Hello World!'),
color: Colors.yellow,
),
),
使用 BoxConstraints
,你可以指定一个小部件可以有多少空间,你可以指定高度/宽度的最小/最大值。
除非指定值,否则 BoxConstraints.expand
使用无限的空间量(也就是使用剩下的所有空间):
ConstrainedBox(
constraints: BoxConstraints.expand(height: 300),
child: const Card(
child: const Text('Hello World!'),
color: Colors.yellow,
),
),
上面👆的写法等同下面👇的写法:
ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity,
maxWidth: double.infinity,
minHeight: 300,
maxHeight: 300,
),
child: const Card(
child: const Text('Hello World!'),
color: Colors.yellow,
),
),
有时候,我们很难设置我们的小部件到正确的大小 -- 比如,它们自由伸展,但是这不是你想要的。
当你在 Column
中使用 CrossAxisAlignment.stretch
的时候,上面的现象就会发生,而你想要的是这个按钮不伸展。
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Align(
child: RaisedButton(
onPressed: () {},
child: const Text('Button'),
),
),
],
),
当你的小部件并不受限你设定的约束时,那么你可以尝试使用 Align
部件包裹它。
Container
是最常用的部件之一 -- 有如下的好处:
当你没有指定 Container
的高度 height
或者宽度 width
的时候,它会自动适配 child
子部件的大小。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Container as a layout')),
body: Container(
color: Colors.yellowAccent,
child: Text("Hi"),
),
);
}
如果你想伸展Container
来适配它的父部件,请为属性高度height
或宽度width
设定值double.infinity
。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Container as a layout')),
body: Container(
height: double.infinity,
width: double.infinity,
color: Colors.yellowAccent,
child: Text("Hi"),
),
);
}
你可以使用 Container
的 color
属性来更改其背景颜色,但是你也可以使用 decoration
和 foregroundDecoration
来更改。(使用这两个属性,你完全可以更改 Container
的样子,这个我们迟点说)。
decoration
总是在 child
属性的后面,而 foregroundDecoration
总是在 child
属性的后面。(这也不一定)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Container.decoration')),
body: Container(
height: double.infinity,
width: double.infinity,
decoration: BoxDecoration(color: Colors.yellowAccent),
child: Text("Hi"),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Container.foregroundDecoration')),
body: Container(
height: double.infinity,
width: double.infinity,
decoration: BoxDecoration(color: Colors.yellowAccent),
foregroundDecoration: BoxDecoration(
color: Colors.red.withOpacity(0.5),
),
child: Text("Hi"),
),
);
}
如果你不想使用 Transform
部件来更改布局,你可以直接使用 Container
中的 transform
属性。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Container.transform')),
body: Container(
height: 300,
width: 300,
transform: Matrix4.rotationZ(pi / 4),
decoration: BoxDecoration(color: Colors.yellowAccent),
child: Text(
"Hi",
textAlign: TextAlign.center,
),
),
);
}
decoration
通常用于更改 Container
部件的外观。
图片作为背景:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('image: DecorationImage')),
body: Center(
child: Container(
height: 200,
width: 200,
decoration: BoxDecoration(
color: Colors.yellow,
image: DecorationImage(
fit: BoxFit.fitWidth,
image: NetworkImage(
'https://flutter.dev/images/catalog-widget-placeholder.png', // 地址已经无效
),
),
),
),
),
);
}
指定 Container
的边框看起来该怎样。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('border: Border')),
body: Center(
child: Container(
height: 200,
width: 200,
decoration: BoxDecoration(
color: Colors.yellow,
border: Border.all(color: Colors.black, width: 3),
),
),
),
);
}
使得边框角变圆。
如果装饰中 shape
属性的值是 BoxShape.circle
,那么 borderRadius
不会起作用。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('borderRadius: BorderRadius')),
body: Center(
child: Container(
height: 200,
width: 200,
decoration: BoxDecoration(
color: Colors.yellow,
border: Border.all(color: Colors.black, width: 3),
borderRadius: BorderRadius.all(Radius.circular(18)),
),
),
),
);
}
BoxDecoration
可以是矩形/正方形或者椭圆/圆形。
对于其他形状,你可以使用 ShapeDecoration
代替 BoxDecoration
。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('shape: BoxShape')),
body: Center(
child: Container(
height: 200,
width: 200,
decoration: BoxDecoration(
color: Colors.yellow,
shape: BoxShape.circle,
),
),
),
);
}
为 Container
添加阴影。
这个参数是一个列表,你可以指定多个不同的阴影并将它们合并在一起。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('boxShadow: List<BoxShadow>')),
body: Center(
child: Container(
height: 200,
width: 200,
decoration: BoxDecoration(
color: Colors.yellow,
boxShadow: const [
BoxShadow(blurRadius: 10),
],
),
),
),
);
}
有三种类型的渐变:LinearGradient
,RadialGradient
和 SweepGradient
。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('gradient: LinearGradient')),
body: Center(
child: Container(
height: 200,
width: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: const [
Colors.red,
Colors.blue,
],
),
),
),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('gradient: RadialGradient')),
body: Center(
child: Container(
height: 200,
width: 200,
decoration: BoxDecoration(
gradient: RadialGradient(
colors: const [Colors.yellow, Colors.blue],
stops: const [0.4, 1.0],
),
),
),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('gradient: SweepGradient')),
body: Center(
child: Container(
height: 200,
width: 200,
decoration: BoxDecoration(
gradient: SweepGradient(
colors: const [
Colors.blue,
Colors.green,
Colors.yellow,
Colors.red,
Colors.blue,
],
stops: const [0.0, 0.25, 0.5, 0.75, 1.0],
),
),
),
),
);
}
backgroundBlendMode
是 BoxDecoration
中最复杂的属性之一。
它负责将 BoxDecoration
中颜色/渐变,以及 BoxDecoration
上的任何内容混合一起。
使用 backgroundBlendMode
, 你可以使用 BlendMode
枚举中指定的一长串算法。
首先,让我们将 BoxDecoration
设置为 foregroundDecoration
,它被绘制在 Container
子部件之上(而 decoration
会绘制在子部件之后)。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('backgroundBlendMode')),
body: Center(
child: Container(
height: 200,
width: 200,
foregroundDecoration: BoxDecoration(
backgroundBlendMode: BlendMode.exclusion,
gradient: LinearGradient(
colors: const [
Colors.red,
Colors.blue,
],
),
),
child: Image.network(
'https://flutter.io/images/catalog-widget-placeholder.png', // 图片 404
),
),
),
);
}
backgroundBlendMode
不仅仅影响它所在的 Container
。
backgroundBlendMode
改变其所在 Container
及其一下部件树的内容的颜色。
下面的代码又一个父部件 Container
来绘制一个 image
,然后有一个子部件 Container
来使用 backgroundBlendMode
,但是你还是获取到和之前的效果。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('backgroundBlendMode')),
body: Center(
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(
'https://flutter.io/images/catalog-widget-placeholder.png', // 404
),
),
),
child: Container(
height: 200,
width: 200,
foregroundDecoration: BoxDecoration(
backgroundBlendMode: BlendMode.exclusion,
gradient: LinearGradient(
colors: const [
Colors.red,
Colors.blue,
],
),
),
),
),
),
);
}
有切角的边框。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('shape: BeveledRectangleBorder')),
body: Center(
child: Material(
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
side: BorderSide(color: Colors.black, width: 4),
),
color: Colors.yellow,
child: Container(
height: 200,
width: 200,
),
),
),
);
}
即便没有足够的空间,当你想要将内容居中,这个部件也是不可替代的。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('SliverFillRemaining')),
body: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
FlutterLogo(size: 200),
Text(
'This is some longest text that should be centered'
'together with the logo',
textAlign: TextAlign.center,
),
],
),
),
],
),
);
}
如果居中的内容没有足够的空间,SliverFillRemaining
将变为可滚动。
如果没使用 SliverFillRemaining
,内容将会像下面这样溢出:
除了对内容居中有用之外,SliverFillRemaining
还会填充剩余视口的可用空间。为此,此部件必须放置在 CustomScrollView
中,并且必须是最后一个 sliver
。
如果没有足够的空间,部件将变为可滚动。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('SliverFillRemaining')),
body: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate(const [
ListTile(title: Text('First item')),
ListTile(title: Text('Second item')),
ListTile(title: Text('Third item')),
ListTile(title: Text('Fourth item')),
]),
),
SliverFillRemaining(
hasScrollBody: false,
child: Container(
color: Colors.yellowAccent,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
FlutterLogo(size: 200),
Text(
'This is some longest text that should be centered'
'together with the logo',
textAlign: TextAlign.center,
),
],
),
),
),
],
),
);
}
SizedBox
是最简单但是最常用的小部件之一。
SizedBox
工作方式跟 ConstrainedBox
有些类似。
SizedBox.expand(
child: Card(
child: Text('Hello World!'),
color: Colors.yellowAccent,
),
),
当需要添加内边距和外边距的时候,你可以选择 Padding
或 Container
小部件。但是,它们可以比添加 Sizedbox
更冗长且可读性更低。
Column(
children: <Widget>[
Icon(Icons.star, size: 50),
const SizedBox(height: 100),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
很多时候,你想通过设置一个 bool
值来隐藏/展示小部件。
|
Widget build(BuildContext context) {
bool isVisible = true; // true or false
return Scaffold(
appBar: AppBar(
title: Text('isVisible = $isVisible'),
),
body: isVisible
? Icon(Icons.star, size: 150)
: const SizedBox(),
);
}
因为 SizedBox
有一个 const
构造函数,所以使用 const SizeBox()
真的很便宜。一种更便宜的解决方案是使用 Opacity
小部件,将其 opacity
值更改为 0.0
。这种解决方案的缺点是给定的小部件只是不可见,但是还是占用空间。
在不同的平台,有些特定的区域,比如安卓的状态栏或者 iPhone X
的刘海区块,我们不应该在其下面绘制内容。
解决方案是使用 SafeArea
小部件。(下面截图是没使用/使用 SafeArea
)
|
Widget build(BuildContext context) {
return Material(
color: Colors.blue,
child: SafeArea(
child: SizedBox.expand(
child: Card(color: Colors.yellowAccent),
),
),
);
}
本文翻译自Flutter Layout Cheat Sheet。代码已经验证,需要留意RaisedButton
已经被ElevatedButton
替代,在现实使用中需要留意。本文重点是其布局思路和技巧。