#頭條創作挑戰賽#
本文同步本人掘金平台的原創翻譯:https://juejin.cn/post/7108530957976043528
banner
你是否需要了解 Flutter 布局的案例?
這裡我将展示我在使用 Flutter 布局的代碼片段。我将通過精美的代碼片段結合可視化的圖形來舉例。
本文注重 Flutter 部件中比較有用的一些來展示,而不是走馬觀花展示一大堆的部件内容。
Row and Column
行(Row)和列(Column)的布局
MainAxisAlignment
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 | Column |
| |
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
複制代碼
Row | Column |
| |
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
複制代碼
Row | Column |
| |
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
複制代碼
Row | Column |
| |
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
複制代碼
Row | Column |
| |
Row /*or Column*/(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
複制代碼
CrossAxisAlignment
如果你需要文本是針對基線對齊,那麼你應該使用 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 | Column |
| |
Row /*or Column*/(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 200),
Icon(Icons.star, size: 50),
],
),
複制代碼
Row | Column |
| |
Row /*or Column*/(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 200),
Icon(Icons.star, size: 50),
],
),
複制代碼
Row | Column |
| |
Row /*or Column*/(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 200),
Icon(Icons.star, size: 50),
],
),
複制代碼
Row | Column |
| |
Row /*or Column*/(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 200),
Icon(Icons.star, size: 50),
],
),
複制代碼
MainAxisSize
Row | Column |
| |
Row /*or Column*/(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
複制代碼
Row | Column |
| |
Row /*or Column*/(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
Icon(Icons.star, size: 50),
],
),
複制代碼
IntrinsicWidth and IntrinsicHeight
在行列布局中,如何使得所有的部件跟寬度/高度最大的部件同寬/同高呢?如下:
我們假有下面的布局:
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
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
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,
),
],
),
複制代碼
ConstrainedBox
預設的,很多部件多盡量使用小空間,比如:
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,
),
),
複制代碼
Align
有時候,我們很難設定我們的小部件到正确的大小 -- 比如,它們自由伸展,但是這不是你想要的。
當你在 Column 中使用 CrossAxisAlignment.stretch 的時候,上面的現象就會發生,而你想要的是這個按鈕不伸展。
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Align(
child: RaisedButton(
onPressed: () {},
child: const Text('Button'),
),
),
],
),
複制代碼
當你的小部件并不受限你設定的限制時,那麼你可以嘗試使用 Align 部件包裹它。
Container
Container 是最常用的部件之一 -- 有如下的好處:
Container as a layout tool
當你沒有指定 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 as decoration
你可以使用 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"),
),
);
}
複制代碼
Container as Transform
如果你不想使用 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,
),
),
);
}
複制代碼
BoxDecoration
decoration 通常用于更改 Container 部件的外觀。
image: DecorationImage
圖檔作為背景:
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', // 位址已經無效
),
),
),
),
),
);
}
複制代碼
border: Border
指定 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),
),
),
),
);
}
複制代碼
borderRadius: BorderRadius
使得邊框角變圓。
如果裝飾中 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)),
),
),
),
);
}
複制代碼
shape: BoxShape
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,
),
),
),
);
}
複制代碼
boxShadow: List
為 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),
],
),
),
),
);
}
複制代碼
gradient
有三種類型的漸變: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
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,
],
),
),
),
),
),
);
}
複制代碼
Material
有切角的邊框。
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,
),
),
),
);
}
複制代碼
Slivers
SliverFillRemaining
即便沒有足夠的空間,當你想要将内容居中,這個部件也是不可替代的。
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,内容将會像下面這樣溢出:
Filling the remaining space
除了對内容居中有用之外,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 是最簡單但是最常用的小部件之一。
SizedBox as ConstrainedBox
SizedBox 工作方式跟 ConstrainedBox 有些類似。
SizedBox.expand(
child: Card(
child: Text('Hello World!'),
color: Colors.yellowAccent,
),
),
複制代碼
SizedBox as padding
當需要添加内邊距和外邊距的時候,你可以選擇 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),
],
),
複制代碼
SizedBox as an Invisible Object
很多時候,你想通過設定一個 bool 值來隐藏/展示小部件。
Show | Hide |
| |
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。這種解決方案的缺點是給定的小部件隻是不可見,但是還是占用空間。
SafeArea
在不同的平台,有些特定的區域,比如安卓的狀态欄或者 iPhone X 的劉海區塊,我們不應該在其下面繪制内容。
解決方案是使用 SafeArea 小部件。(下面截圖是沒使用/使用 SafeArea)
without | with |
| |
Widget build(BuildContext context) {
return Material(
color: Colors.blue,
child: SafeArea(
child: SizedBox.expand(
child: Card(color: Colors.yellowAccent),
),
),
);
}
複制代碼
代碼已經驗證,需要留意 RaisedButton 已經被 ElevatedButton 替代,在現實使用中需要留意。本文重點是其布局思路和技巧。