1.效果图:
2.分析布局
布局建议通过以下方式来分析:
- 找出行和列
- 布局包含网格吗?
- 有重叠的元素吗?
- 是否需要选项卡?
- 注意需要对齐、填充和边框的区域
本例的如下图的分析,可知:四个元素排列成一列:一个图像,两个行和一个文本块。
标题部分有三个子项:一列文字,一个星形图标和一个数字。它的第一个子项,列,包含2行文字。 第一列占用大量空间,所以它必须包装在Expanded widget中。
下面的按钮部分也有3个子项:每个子项都是一个包含图标和文本的列:
3.实现布局
为了简化实现,可以采用逐个实现,再整合各个部分。一旦拆分好布局,最简单的就是采取自上而下的方法来实现它。为了最大限度地减少深度嵌套布局代码的视觉混淆,将一些实现放置在变量和函数中。
3.1.实现图像部分
body: ListView(
children: <Widget>[
Image.asset(
'assets/images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,// BoxFit.cover 告诉框架,图像应该尽可能小,但覆盖整个渲染框
),
// ...
],
)
BoxFit.cover 告诉框架,图像应该尽可能小,但覆盖整个渲染框。
3.2.实现标题行
标题部分,有三个子项:一列文字,一个星形图标和一个数字。它的第一个子项,列,包含2行文字。 第一列占用大量空间,所以它必须包装在Expanded widget中。
- 首先,构建标题部分左边栏。将Column(列)放入Expanded中会拉伸该列以使用该行中的所有剩余空闲空间。 设置crossAxisAlignment属性值为CrossAxisAlignment.start,这会将该列中的子项左对齐。
- 将第一行文本放入Container中,然后底部添加8像素填充。列中的第二个子项(也是文本)显示为灰色。
- 标题行中的最后两项是一个红色的星形图标和文字“41”。将整行放在容器中,并沿着每个边缘填充32像素。
/// 标题栏
Widget titleSection = Container(
padding: const EdgeInsets.all(32),
child: Row(
children: <Widget>[
Expanded(
// Expanded这个控件会把同级别的控件,在父控件中填充满整个父控件。
// 在Expanded中的子控件,会自动缩放,填充使得整个父控件被填充,如果添加文字之后,Expanded 它的大小会受到文字的影响
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'Kandersteg Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
)
],
),
),
FavoriteWidget(),
],
),
);
标题行中的最后两项是一个红色的星形图标和文字“41”,我们写成一个独立的控件:
class FavoriteWidget extends StatefulWidget {
@override
_FavoriteWidgetState createState() => _FavoriteWidgetState();
}
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
int _favoriteCount = 41;
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)),
color: Colors.red[500],
onPressed: _toggleFavorite,
),
),
SizedBox(
width: 18,
child: Container(
child: Text('$_favoriteCount'),
),
)
],
);
}
}
3.3.实现按钮行
按钮部分包含3个使用相同布局的列 - 上面一个图标,下面一行文本。该行中的列平均分布行空间, 文本和图标颜色为主题中的primary color,它在应用程序的build()方法中设置为蓝色。由于构建每个列的代码几乎是相同的,因此使用一个嵌套函数,如buildButtonColumn,它会创建一个颜色为primary color,包含一个Icon和Text的 Widget 列。
Column _buildButtonColumn(Color color, IconData icon, String label) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(icon, color: color),
Container(
margin: const EdgeInsets.only(top: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
);
}
通过调用函数并传递icon和文本来构建这些列。然后在行的主轴方向通过 MainAxisAlignment.spaceEvenly 平均的分配每个列占据的行空间:
/// 按钮组
Color color = Theme.of(context).primaryColor;
Widget buttonSection = Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buildButtonColumn(color, Icons.call, 'CALL'),
_buildButtonColumn(color, Icons.near_me, 'ROUTE'),
_buildButtonColumn(color, Icons.share, 'SHARE'),
],
),
);
3.4.实现文本部分
将文本放入容器中,以便沿每条边添加32像素的填充。softwrap属性表示文本是否应在软换行符(例如句点或逗号)之间断开。
/// 文本框
Widget textSection = Container(
padding: const EdgeInsets.all(32),
child: Text(
'''
Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese Alps. Situated 1,578 meters above sea level, it is one of the larger Alpine Lakes. A gondola ride from Kandersteg, followed by a half-hour walk through pastures and pine forest, leads you to the lake, which warms to 20 degrees Celsius in the summer. Activities enjoyed here include rowing, and riding the summer toboggan run.
''',
softWrap: true, //是否自动换行,若为false,文字将不考虑容器大小,单行显示,超出屏幕部分将默认截断处理
),
);
4.整合
将上面这些widget放置到ListView中,而不是列中,因为在小设备上运行应用程序时,ListView会自动滚动。
MaterialApp(
title: 'Flutter layout demo',
home: Scaffold(
appBar: AppBar(
title: Text('flutter layout demo'),
),
body: ListView(
children: <Widget>[
Image.asset(
'assets/images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,// BoxFit.cover 告诉框架,图像应该尽可能小,但覆盖整个渲染框
),
titleSection,
buttonSection,
textSection,
],
),
),
);
5.补充说明
5.1.
name: interactive
description: >
Sample app from "Adding interactivity", https://flutter.io/docs/development/ui/interactive.
version: 1.0.0
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/images/
目录:
├── assets
│ └── images
├── lib
│ ├── main.dart
完整代码:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
void main() => runApp(DemoApp());
class DemoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Image Picker Demo',
home: IntroductionPage(),
);
}
}
class IntroductionPage extends StatelessWidget {
Column _buildButtonColumn(Color color, IconData icon, String label) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(icon, color: color),
Container(
margin: const EdgeInsets.only(top: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
/// 标题栏
Widget titleSection = Container(
padding: const EdgeInsets.all(32),
child: Row(
children: <Widget>[
Expanded(
// Expanded这个控件会把同级别的控件,在父控件中填充满整个父控件。
// 在Expanded中的子控件,会自动缩放,填充使得整个父控件被填充,如果添加文字之后,Expanded 它的大小会受到文字的影响
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'Kandersteg Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
)
],
),
),
FavoriteWidget(),
],
),
);
/// 按钮组
Color color = Theme.of(context).primaryColor;
Widget buttonSection = Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buildButtonColumn(color, Icons.call, 'CALL'),
_buildButtonColumn(color, Icons.near_me, 'ROUTE'),
_buildButtonColumn(color, Icons.share, 'SHARE'),
],
),
);
/// 文本框
Widget textSection = Container(
padding: const EdgeInsets.all(32),
child: Text(
'''
Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese Alps. Situated 1,578 meters above sea level, it is one of the larger Alpine Lakes. A gondola ride from Kandersteg, followed by a half-hour walk through pastures and pine forest, leads you to the lake, which warms to 20 degrees Celsius in the summer. Activities enjoyed here include rowing, and riding the summer toboggan run.
''',
softWrap: true, //是否自动换行,若为false,文字将不考虑容器大小,单行显示,超出屏幕部分将默认截断处理
),
);
return MaterialApp(
title: 'Flutter layout demo',
home: Scaffold(
appBar: AppBar(
title: Text('flutter layout demo'),
),
body: ListView(
children: <Widget>[
Image.asset(
'assets/images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,// BoxFit.cover 告诉框架,图像应该尽可能小,但覆盖整个渲染框
),
titleSection,
buttonSection,
textSection,
],
),
),
);
}
}
class FavoriteWidget extends StatefulWidget {
@override
_FavoriteWidgetState createState() => _FavoriteWidgetState();
}
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
int _favoriteCount = 41;
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)),
color: Colors.red[500],
onPressed: _toggleFavorite,
),
),
SizedBox(
width: 18,
child: Container(
child: Text('$_favoriteCount'),
),
)
],
);
}
}