Flutter 笔记(下)
四、布局组件
5. 基础容器组件
Container(容器组件)
定义: Container 是最常用的容器组件,可以包含一个子组件,并提供装饰、定位、尺寸等功能。
常用属性:
Container(
// 尺寸
width: 200,
height: 100,
// 内边距
padding: EdgeInsets.all(16),
// 外边距
margin: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
// 对齐方式
alignment: Alignment.center,
// 装饰(背景色、边框、圆角等)
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.black, width: 2),
boxShadow: [
BoxShadow(
color: Colors.grey,
blurRadius: 5,
offset: Offset(2, 2),
),
],
),
// 子组件
child: Text('Hello'),
)
EdgeInsets 用法:
// 所有方向相同
EdgeInsets.all(16)
// 上下左右分别设置
EdgeInsets.only(left: 10, top: 20, right: 10, bottom: 20)
// 水平和垂直
EdgeInsets.symmetric(horizontal: 20, vertical: 10)
// 从 LTRB(左、上、右、下)
EdgeInsets.fromLTRB(10, 20, 10, 20)
实际示例:
Container(
width: 300,
height: 200,
margin: EdgeInsets.all(20),
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
),
borderRadius: BorderRadius.circular(15),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.star, size: 50, color: Colors.white),
SizedBox(height: 10),
Text(
'精美卡片',
style: TextStyle(color: Colors.white, fontSize: 24),
),
],
),
)
Center(居中组件)
定义: 将子组件在父组件中居中显示。
用法:
Center(
child: Text('我在中间'),
)
// 等价于
Container(
alignment: Alignment.center,
child: Text('我在中间'),
)
Align(对齐组件)
定义: 可以精确控制子组件的对齐位置。
Alignment 常用值:
Align(
alignment: Alignment.topLeft, // 左上
// alignment: Alignment.topCenter, // 上中
// alignment: Alignment.topRight, // 右上
// alignment: Alignment.centerLeft, // 左中
// alignment: Alignment.center, // 中心
// alignment: Alignment.centerRight, // 右中
// alignment: Alignment.bottomLeft, // 左下
// alignment: Alignment.bottomCenter, // 下中
// alignment: Alignment.bottomRight, // 右下
child: Text('对齐位置'),
)
自定义对齐:
Align(
// x, y 取值范围 -1.0 到 1.0
// (-1, -1) 为左上角,(1, 1) 为右下角
alignment: Alignment(0.5, 0.5),
child: Text('自定义位置'),
)
Padding(内边距组件)
定义: 为子组件添加内边距。
用法:
Padding(
padding: EdgeInsets.all(20),
child: Text('有内边距的文本'),
)
// 不同方向的内边距
Padding(
padding: EdgeInsets.only(left: 16, top: 8),
child: Text('左上有内边距'),
)
提示: Padding 比 Container 更轻量,如果只需要内边距,使用 Padding 更高效。
6. 线性布局
Column(垂直布局)
定义: 将子组件垂直排列。
核心属性:
Column(
// 主轴对齐(垂直方向)
mainAxisAlignment: MainAxisAlignment.start,
// MainAxisAlignment.start - 顶部对齐
// MainAxisAlignment.end - 底部对齐
// MainAxisAlignment.center - 居中对齐
// MainAxisAlignment.spaceBetween - 两端对齐,中间平分空间
// MainAxisAlignment.spaceAround - 每个组件周围有相等空间
// MainAxisAlignment.spaceEvenly - 空间均匀分布
// 交叉轴对齐(水平方向)
crossAxisAlignment: CrossAxisAlignment.center,
// CrossAxisAlignment.start - 左对齐
// CrossAxisAlignment.end - 右对齐
// CrossAxisAlignment.center - 居中对齐
// CrossAxisAlignment.stretch - 拉伸填充
// 主轴尺寸
mainAxisSize: MainAxisSize.max, // max 或 min
children: [
Text('第一行'),
Text('第二行'),
Text('第三行'),
],
)
实际示例:
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 100,
height: 50,
color: Colors.red,
child: Center(child: Text('Box 1')),
),
Container(
width: 150,
height: 50,
color: Colors.green,
child: Center(child: Text('Box 2')),
),
Container(
width: 120,
height: 50,
color: Colors.blue,
child: Center(child: Text('Box 3')),
),
],
)
Row(水平布局)
定义: 将子组件水平排列。
用法:(属性与 Column 类似,但主轴和交叉轴相反)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.home, size: 30),
Icon(Icons.search, size: 30),
Icon(Icons.person, size: 30),
],
)
组合使用示例:
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('标题'),
Icon(Icons.more_vert),
],
),
SizedBox(height: 10),
Row(
children: [
Icon(Icons.star, color: Colors.yellow),
Icon(Icons.star, color: Colors.yellow),
Icon(Icons.star, color: Colors.yellow),
Text('4.5'),
],
),
],
)
Expanded 和 Flexible
Expanded(填充可用空间)
定义: 在 Row 或 Column 中,按比例分配剩余空间。
用法:
Row(
children: [
// 固定宽度
Container(
width: 50,
height: 50,
color: Colors.red,
),
// 占据剩余空间
Expanded(
child: Container(
height: 50,
color: Colors.green,
child: Center(child: Text('填充剩余空间')),
),
),
// 固定宽度
Container(
width: 50,
height: 50,
color: Colors.blue,
),
],
)
flex 属性(按比例分配):
Row(
children: [
Expanded(
flex: 1, // 占 1 份
child: Container(
height: 50,
color: Colors.red,
child: Center(child: Text('1')),
),
),
Expanded(
flex: 2, // 占 2 份
child: Container(
height: 50,
color: Colors.green,
child: Center(child: Text('2')),
),
),
Expanded(
flex: 1, // 占 1 份
child: Container(
height: 50,
color: Colors.blue,
child: Center(child: Text('1')),
),
),
],
)
// 比例:1:2:1
Flexible(灵活布局)
定义: 与 Expanded 类似,但不强制填充空间。
区别:
Row(
children: [
// Flexible 不强制占满
Flexible(
child: Container(
width: 100, // 可以有自己的宽度
height: 50,
color: Colors.red,
),
),
// Expanded 强制占满
Expanded(
child: Container(
height: 50,
color: Colors.green,
),
),
],
)
Flex 布局
定义:
Row 和 Column 的父类,可以通过 direction 属性动态决定方向。
用法:
Flex(
direction: Axis.horizontal, // 或 Axis.vertical
children: [
Expanded(
flex: 1,
child: Container(color: Colors.red, height: 50),
),
Expanded(
flex: 2,
child: Container(color: Colors.green, height: 50),
),
],
)
7. 弹性布局(Flex Layout)
使用场景: 当需要根据可用空间动态调整子组件大小时使用。
完整示例:
class FlexExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// 等比例分配
Container(
height: 100,
color: Colors.grey[300],
child: Row(
children: [
Expanded(flex: 1, child: _buildBox(Colors.red, '1')),
Expanded(flex: 1, child: _buildBox(Colors.green, '1')),
Expanded(flex: 1, child: _buildBox(Colors.blue, '1')),
],
),
),
SizedBox(height: 20),
// 不同比例分配
Container(
height: 100,
color: Colors.grey[300],
child: Row(
children: [
Expanded(flex: 1, child: _buildBox(Colors.red, '1')),
Expanded(flex: 2, child: _buildBox(Colors.green, '2')),
Expanded(flex: 3, child: _buildBox(Colors.blue, '3')),
],
),
),
],
);
}
Widget _buildBox(Color color, String text) {
return Container(
color: color,
child: Center(
child: Text(
text,
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
);
}
}
Flexible 和 Expanded 详解:
1. Expanded(占满剩余空间):
Row(
children: [
Container(width: 100, height: 50, color: Colors.red),
Expanded(
child: Container(height: 50, color: Colors.green), // 占满剩余空间
),
Container(width: 100, height: 50, color: Colors.blue),
],
)
2. Flexible(灵活空间分配):
Row(
children: [
// 不会自动填充,根据内容大小
Flexible(
child: Container(
height: 50,
color: Colors.red,
child: Text('短文本'),
),
),
// fit: FlexFit.tight 等同于 Expanded
Flexible(
fit: FlexFit.tight,
child: Container(height: 50, color: Colors.green),
),
],
)
3. Expanded vs Flexible:
// Expanded = Flexible(fit: FlexFit.tight)
// 区别:
// - Expanded:必须填满分配的空间
// - Flexible:可以小于分配的空间
Row(
children: [
Flexible(
flex: 1,
child: Container(
height: 50,
color: Colors.red,
child: Text('我只占我需要的空间'),
),
),
Expanded(
flex: 1,
child: Container(
height: 50,
color: Colors.green,
child: Text('我占满分配的空间'),
),
),
],
)
4. flex 比例:
Column(
children: [
Expanded(
flex: 1, // 占 1/6 的空间
child: Container(color: Colors.red),
),
Expanded(
flex: 2, // 占 2/6 的空间
child: Container(color: Colors.green),
),
Expanded(
flex: 3, // 占 3/6 的空间
child: Container(color: Colors.blue),
),
],
)
8. 间距组件
SizedBox(固定尺寸盒子)
定义: 创建固定宽高的空间,常用于添加间距。
用法:
Column(
children: [
Text('第一行'),
SizedBox(height: 20), // 垂直间距
Text('第二行'),
SizedBox(height: 40),
Text('第三行'),
],
)
Row(
children: [
Text('左侧'),
SizedBox(width: 30), // 水平间距
Text('右侧'),
],
)
作为固定尺寸容器:
SizedBox(
width: 200,
height: 100,
child: ElevatedButton(
onPressed: () {},
child: Text('固定大小按钮'),
),
)
Spacer(弹性空白)
定义: 在 Flex 布局中创建可伸缩的空白空间。
用法:
Row(
children: [
Text('左侧'),
Spacer(), // 占据所有剩余空间
Text('右侧'),
],
)
// 多个 Spacer 按比例分配空间
Row(
children: [
Text('左'),
Spacer(flex: 1), // 占 1/3
Text('中'),
Spacer(flex: 2), // 占 2/3
Text('右'),
],
)
Divider(分隔线)
定义: 水平分隔线。
用法:
Column(
children: [
Text('项目 1'),
Divider(), // 默认分隔线
Text('项目 2'),
Divider(
height: 20, // 分隔线占据的高度
thickness: 2, // 线的粗细
indent: 16, // 左侧缩进
endIndent: 16, // 右侧缩进
color: Colors.grey,
),
Text('项目 3'),
],
)
VerticalDivider(垂直分隔线):
Row(
children: [
Expanded(child: Text('左侧')),
VerticalDivider(
width: 20,
thickness: 2,
color: Colors.grey,
),
Expanded(child: Text('右侧')),
],
)
9. 显示隐藏控制
Visibility(显示隐藏组件)
定义: 控制组件的显示和隐藏。
用法:
class VisibilityDemo extends StatefulWidget {
@override
_VisibilityDemoState createState() => _VisibilityDemoState();
}
class _VisibilityDemoState extends State<VisibilityDemo> {
bool _isVisible = true;
@override
Widget build(BuildContext context) {
return Column(
children: [
Visibility(
visible: _isVisible,
child: Container(
width: 200,
height: 100,
color: Colors.blue,
child: Center(child: Text('可见的组件')),
),
),
ElevatedButton(
onPressed: () {
setState(() {
_isVisible = !_isVisible;
});
},
child: Text('切换显示'),
),
],
);
}
}
保持空间的隐藏:
Visibility(
visible: false,
maintainSize: true, // 保持尺寸
maintainAnimation: true, // 保持动画
maintainState: true, // 保持状态
child: Container(
width: 200,
height: 100,
color: Colors.blue,
),
)
Opacity(透明度控制)
定义: 控制组件的透明度。
用法:
class OpacityDemo extends StatefulWidget {
@override
_OpacityDemoState createState() => _OpacityDemoState();
}
class _OpacityDemoState extends State<OpacityDemo> {
double _opacity = 1.0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Opacity(
opacity: _opacity, // 0.0 完全透明,1.0 完全不透明
child: Container(
width: 200,
height: 100,
color: Colors.blue,
child: Center(child: Text('透明度控制')),
),
),
Slider(
value: _opacity,
min: 0.0,
max: 1.0,
onChanged: (value) {
setState(() {
_opacity = value;
});
},
),
],
);
}
}
Offstage(完全隐藏)
定义: 完全移除组件,不占据空间,不参与布局。
用法:
Offstage(
offstage: true, // true: 隐藏,false: 显示
child: Container(
width: 200,
height: 100,
color: Colors.blue,
),
)
Visibility vs Opacity vs Offstage:
Column(
children: [
// Visibility: 隐藏后不占空间,可以选择是否保持状态
Visibility(visible: false, child: Text('Visibility')),
// Opacity: 隐藏后仍占空间,可以渐变动画
Opacity(opacity: 0.0, child: Text('Opacity')),
// Offstage: 完全移除,不占空间,不参与布局
Offstage(offstage: true, child: Text('Offstage')),
],
)
10. 层叠布局
Stack(堆叠组件)
定义: 允许子组件堆叠在一起,后面的组件会覆盖前面的组件。
基础用法:
Stack(
children: [
// 底层
Container(
width: 300,
height: 300,
color: Colors.blue,
),
// 中层
Container(
width: 200,
height: 200,
color: Colors.green,
),
// 顶层
Container(
width: 100,
height: 100,
color: Colors.red,
),
],
)
alignment 属性:
Stack(
alignment: Alignment.center, // 所有子组件居中对齐
children: [
Container(width: 300, height: 300, color: Colors.blue),
Container(width: 200, height: 200, color: Colors.green),
Container(width: 100, height: 100, color: Colors.red),
],
)
Positioned(定位组件)
定义: 在 Stack 中精确控制子组件的位置。
用法:
Stack(
children: [
// 背景
Container(
width: 300,
height: 300,
color: Colors.grey[300],
),
// 左上角
Positioned(
left: 10,
top: 10,
child: Container(
width: 50,
height: 50,
color: Colors.red,
),
),
// 右上角
Positioned(
right: 10,
top: 10,
child: Container(
width: 50,
height: 50,
color: Colors.green,
),
),
// 底部居中
Positioned(
left: 0,
right: 0,
bottom: 10,
child: Container(
height: 50,
color: Colors.blue,
child: Center(child: Text('底部栏')),
),
),
],
)
实际应用 - 图片上的角标:
Stack(
children: [
// 主图片
Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover,
),
// 右上角角标
Positioned(
right: 5,
top: 5,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'NEW',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
),
// 底部信息栏
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: EdgeInsets.all(8),
color: Colors.black.withOpacity(0.5),
child: Text(
'商品标题',
style: TextStyle(color: Colors.white),
),
),
),
],
)
9. 流式布局
Wrap(自动换行组件)
定义: 当一行空间不足时,自动换行显示子组件。
用法:
Wrap(
spacing: 8, // 主轴方向间距(水平)
runSpacing: 10, // 交叉轴方向间距(垂直)
alignment: WrapAlignment.start, // 对齐方式
children: [
Chip(label: Text('标签1')),
Chip(label: Text('标签2')),
Chip(label: Text('标签3')),
Chip(label: Text('标签4')),
Chip(label: Text('标签5')),
Chip(label: Text('标签6')),
],
)
实际应用 - 标签云:
class TagCloud extends StatelessWidget {
final List<String> tags = [
'Flutter', 'Dart', 'Material Design', 'iOS', 'Android',
'Web', 'Desktop', 'Mobile', 'UI', 'UX',
];
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 10,
runSpacing: 10,
children: tags.map((tag) {
return Chip(
label: Text(tag),
backgroundColor: Colors.blue[100],
deleteIcon: Icon(Icons.close, size: 18),
onDeleted: () {
print('删除: $tag');
},
);
}).toList(),
);
}
}
Wrap 方向:
Wrap(
direction: Axis.vertical, // 垂直方向排列,自动换列
spacing: 8,
runSpacing: 10,
children: [
Container(width: 50, height: 50, color: Colors.red),
Container(width: 50, height: 50, color: Colors.green),
Container(width: 50, height: 50, color: Colors.blue),
],
)
10. 滚动布局基础
滚动布局类型:
- SingleChildScrollView - 单个子组件可滚动
- ListView - 列表滚动
- GridView - 网格滚动
- CustomScrollView - 自定义滚动(使用 Sliver)
- PageView - 整页滚动
何时使用滚动:
- 内容超出屏幕可见区域
- 需要上下或左右滑动查看更多内容
- 列表、网格等长列表场景
详细内容见第六章"滚动组件"
11. 尺寸约束组件
AspectRatio(宽高比)
定义: 强制子组件保持指定的宽高比。
基础用法:
AspectRatio(
aspectRatio: 16 / 9, // 宽高比 16:9
child: Container(
color: Colors.blue,
child: Center(child: Text('16:9 宽高比')),
),
)
实际应用 - 视频播放器容器:
Container(
width: double.infinity,
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Colors.black,
child: Center(
child: Icon(Icons.play_circle_outline, color: Colors.white, size: 64),
),
),
),
)
不同宽高比示例:
Column(
children: [
AspectRatio(
aspectRatio: 1, // 1:1 正方形
child: Container(color: Colors.red),
),
SizedBox(height: 10),
AspectRatio(
aspectRatio: 16 / 9, // 16:9 横屏视频
child: Container(color: Colors.green),
),
SizedBox(height: 10),
AspectRatio(
aspectRatio: 4 / 3, // 4:3 传统显示器
child: Container(color: Colors.blue),
),
],
)
FractionallySizedBox(相对尺寸)
定义: 根据父容器的尺寸设置子组件的大小比例。
基础用法:
Container(
width: 300,
height: 200,
color: Colors.grey[300],
child: FractionallySizedBox(
widthFactor: 0.5, // 宽度是父容器的50%
heightFactor: 0.8, // 高度是父容器的80%
alignment: Alignment.center,
child: Container(
color: Colors.blue,
child: Center(child: Text('50% x 80%')),
),
),
)
实际应用 - 响应式布局:
// 在不同屏幕上保持相对大小
FractionallySizedBox(
widthFactor: 0.9, // 屏幕宽度的90%
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
blurRadius: 10,
offset: Offset(0, 5),
),
],
),
child: Text('卡片内容'),
),
)
不同对齐方式:
Container(
width: 300,
height: 200,
color: Colors.grey[300],
child: FractionallySizedBox(
widthFactor: 0.6,
heightFactor: 0.6,
alignment: Alignment.topLeft, // 左上角对齐
child: Container(color: Colors.red),
),
)
ConstrainedBox(尺寸约束)
定义: 对子组件的尺寸施加额外的约束条件。
基础用法:
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 100, // 最小宽度
maxWidth: 200, // 最大宽度
minHeight: 50, // 最小高度
maxHeight: 100, // 最大高度
),
child: Container(
color: Colors.blue,
child: Text('受约束的容器'),
),
)
只约束宽度:
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 200,
maxWidth: 400,
),
child: ElevatedButton(
onPressed: () {},
child: Text('按钮'),
),
)
只约束高度:
ConstrainedBox(
constraints: BoxConstraints(
minHeight: 100,
maxHeight: 200,
),
child: TextField(
maxLines: null,
decoration: InputDecoration(hintText: '多行输入框'),
),
)
实际应用 - 限制图片大小:
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 300,
maxHeight: 300,
),
child: Image.network(
'https://example.com/large-image.jpg',
fit: BoxFit.contain,
),
)
BoxConstraints 常用构造函数:
// 紧约束(固定尺寸)
BoxConstraints.tight(Size(200, 100))
// 松约束(最大尺寸)
BoxConstraints.loose(Size(300, 200))
// 只约束宽度
BoxConstraints.tightFor(width: 200)
// 扩展到最大
BoxConstraints.expand(width: 200, height: 100)
12. 裁剪组件
ClipRRect(圆角裁剪)
定义: 使用圆角矩形裁剪子组件。
基础用法:
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'https://picsum.photos/200/200',
width: 200,
height: 200,
fit: BoxFit.cover,
),
)
不同圆角样式:
// 完全圆角
ClipRRect(
borderRadius: BorderRadius.circular(100),
child: Image.network(url, width: 200, height: 200),
)
// 只圆角某些角
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
child: Image.network(url, width: 200, height: 200),
)
// 上下左右不同圆角
ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
)
实际应用 - 卡片顶部圆角图片:
Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: Image.network(
'https://picsum.photos/400/200',
width: double.infinity,
height: 200,
fit: BoxFit.cover,
),
),
Padding(
padding: EdgeInsets.all(16),
child: Text('卡片内容'),
),
],
),
)
ClipOval(圆形裁剪)
定义: 使用椭圆或圆形裁剪子组件。
基础用法:
ClipOval(
child: Image.network(
'https://picsum.photos/200/200',
width: 100,
height: 100,
fit: BoxFit.cover,
),
)
实际应用 - 圆形头像:
ClipOval(
child: Container(
width: 80,
height: 80,
color: Colors.blue,
child: Image.network(
'https://example.com/avatar.jpg',
fit: BoxFit.cover,
),
),
)
椭圆形裁剪:
ClipOval(
child: Container(
width: 200,
height: 100, // 不同的宽高产生椭圆
color: Colors.blue,
child: Center(child: Text('椭圆')),
),
)
ClipPath(自定义路径裁剪)
定义: 使用自定义路径裁剪子组件。
三角形裁剪:
ClipPath(
clipper: TriangleClipper(),
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
)
// 自定义裁剪器
class TriangleClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.moveTo(size.width / 2, 0);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
波浪形裁剪:
ClipPath(
clipper: WaveClipper(),
child: Container(
width: double.infinity,
height: 200,
color: Colors.blue,
),
)
class WaveClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(0, size.height - 40);
var firstControlPoint = Offset(size.width / 4, size.height);
var firstEndPoint = Offset(size.width / 2, size.height - 40);
path.quadraticBezierTo(
firstControlPoint.dx,
firstControlPoint.dy,
firstEndPoint.dx,
firstEndPoint.dy,
);
var secondControlPoint = Offset(size.width * 3 / 4, size.height - 80);
var secondEndPoint = Offset(size.width, size.height - 40);
path.quadraticBezierTo(
secondControlPoint.dx,
secondControlPoint.dy,
secondEndPoint.dx,
secondEndPoint.dy,
);
path.lineTo(size.width, 0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
实际应用 - 优惠券卡片:
ClipPath(
clipper: CouponClipper(),
child: Container(
width: 300,
height: 100,
color: Colors.orange,
child: Center(child: Text('优惠券', style: TextStyle(fontSize: 24))),
),
)
class CouponClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(0, size.height);
path.lineTo(size.width - 20, size.height);
path.lineTo(size.width, size.height / 2);
path.lineTo(size.width - 20, 0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
ClipRect(矩形裁剪)
定义: 使用矩形裁剪子组件,通常配合 Align 使用。
基础用法:
ClipRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: 0.5, // 只显示上半部分
child: Image.network(
'https://picsum.photos/200/200',
width: 200,
height: 200,
),
),
)
实际应用 - 局部图片展示:
ClipRect(
child: Align(
alignment: Alignment.center,
widthFactor: 0.8,
heightFactor: 0.8,
child: Image.network(url),
),
)
五、基础组件
11. 文本组件
Text(文本组件)
定义: 显示单一样式的文本内容。
基础用法:
Text('Hello Flutter')
常用属性:
Text(
'这是一段文本',
style: TextStyle(
fontSize: 20, // 字体大小
fontWeight: FontWeight.bold, // 字体粗细
color: Colors.blue, // 文字颜色
letterSpacing: 2.0, // 字母间距
height: 1.5, // 行高(倍数)
decoration: TextDecoration.underline, // 下划线
decorationColor: Colors.red, // 装饰线颜色
fontStyle: FontStyle.italic, // 斜体
),
textAlign: TextAlign.center, // 对齐方式
maxLines: 2, // 最大行数
overflow: TextOverflow.ellipsis, // 溢出处理(省略号)
softWrap: true, // 是否自动换行
)
TextStyle 常用属性:
TextStyle(
// 字体大小和粗细
fontSize: 16,
fontWeight: FontWeight.w400, // w100-w900 或 bold, normal
// 颜色
color: Colors.black,
backgroundColor: Colors.yellow,
// 间距
letterSpacing: 1.0, // 字母间距
wordSpacing: 2.0, // 单词间距
height: 1.2, // 行高
// 装饰
decoration: TextDecoration.underline, // none, underline, overline, lineThrough
decorationStyle: TextDecorationStyle.solid, // solid, double, dotted, dashed, wavy
decorationColor: Colors.red,
decorationThickness: 2.0,
// 字体
fontFamily: 'Roboto',
fontStyle: FontStyle.italic, // normal 或 italic
// 阴影
shadows: [
Shadow(
color: Colors.grey,
offset: Offset(2, 2),
blurRadius: 3,
),
],
)
文本对齐:
Text(
'文本对齐',
textAlign: TextAlign.left, // 左对齐
// textAlign: TextAlign.center, // 居中
// textAlign: TextAlign.right, // 右对齐
// textAlign: TextAlign.justify, // 两端对齐
)
文本溢出处理:
Text(
'这是一段很长很长很长很长很长很长的文本',
maxLines: 1,
overflow: TextOverflow.ellipsis, // 省略号
// overflow: TextOverflow.clip, // 裁剪
// overflow: TextOverflow.fade, // 渐隐
// overflow: TextOverflow.visible, // 可见(溢出)
)
RichText(富文本)
定义: 可以在一段文本中使用多种不同的样式。
基础用法:
RichText(
text: TextSpan(
text: '默认样式 ',
style: TextStyle(color: Colors.black, fontSize: 16),
children: [
TextSpan(
text: '粗体 ',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(
text: '红色 ',
style: TextStyle(color: Colors.red),
),
TextSpan(
text: '大字号',
style: TextStyle(fontSize: 24),
),
],
),
)
实际应用示例:
class PriceDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
style: TextStyle(color: Colors.black, fontSize: 16),
children: [
TextSpan(text: '原价 '),
TextSpan(
text: '¥199',
style: TextStyle(
decoration: TextDecoration.lineThrough,
color: Colors.grey,
),
),
TextSpan(text: ' 现价 '),
TextSpan(
text: '¥99',
style: TextStyle(
color: Colors.red,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}
带点击事件的富文本:
RichText(
text: TextSpan(
text: '请阅读并同意 ',
style: TextStyle(color: Colors.black),
children: [
TextSpan(
text: '《用户协议》',
style: TextStyle(color: Colors.blue),
recognizer: TapGestureRecognizer()
..onTap = () {
print('点击了用户协议');
},
),
TextSpan(text: ' 和 '),
TextSpan(
text: '《隐私政策》',
style: TextStyle(color: Colors.blue),
recognizer: TapGestureRecognizer()
..onTap = () {
print('点击了隐私政策');
},
),
],
),
)
12. 图片组件
Image(图片组件)
定义: 用于显示图片,支持多种图片来源。
1. 加载网络图片:
Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover, // 填充方式
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Icon(Icons.error, size: 50, color: Colors.red);
},
)
2. 加载本地资源图片:
首先在 pubspec.yaml 中配置:
flutter:
assets:
- assets/images/
- assets/icons/logo.png
然后使用:
Image.asset(
'assets/images/logo.png',
width: 100,
height: 100,
)
3. 加载文件系统图片:
Image.file(
File('/path/to/image.jpg'),
width: 200,
height: 200,
)
4. 加载内存图片:
Image.memory(
bytes, // Uint8List
width: 200,
height: 200,
)
BoxFit 图片填充方式
Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover,
// BoxFit.cover - 充满容器,可能裁剪
// BoxFit.contain - 完整显示,可能留白
// BoxFit.fill - 拉伸填满,可能变形
// BoxFit.fitWidth - 宽度填满
// BoxFit.fitHeight - 高度填满
// BoxFit.none - 原始大小
// BoxFit.scaleDown - 缩小以适应,不放大
)
实际应用 - 圆形头像:
ClipOval(
child: Image.network(
'https://example.com/avatar.jpg',
width: 80,
height: 80,
fit: BoxFit.cover,
),
)
// 或者使用 CircleAvatar
CircleAvatar(
radius: 40,
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
)
圆角图片:
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover,
),
)
13. 文本输入组件
TextField(文本输入框)
定义: 用于接收用户输入的文本。
基础用法:
TextField(
decoration: InputDecoration(
labelText: '用户名',
hintText: '请输入用户名',
prefixIcon: Icon(Icons.person),
),
)
常用属性:
TextField(
// 控制器
controller: TextEditingController(),
// 装饰
decoration: InputDecoration(
labelText: '标签', // 浮动标签
hintText: '提示文本', // 占位符
helperText: '帮助文本', // 底部帮助
errorText: '错误提示', // 错误信息
// 图标
prefixIcon: Icon(Icons.person), // 前置图标
suffixIcon: Icon(Icons.clear), // 后置图标
// 边框
border: OutlineInputBorder(), // 外边框
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2),
),
// 填充
filled: true,
fillColor: Colors.grey[200],
// 内边距
contentPadding: EdgeInsets.all(16),
),
// 键盘类型
keyboardType: TextInputType.text,
// TextInputType.number - 数字键盘
// TextInputType.phone - 电话键盘
// TextInputType.emailAddress - 邮箱键盘
// TextInputType.url - URL 键盘
// 输入行为
maxLines: 1, // 最大行数
maxLength: 20, // 最大字符数
obscureText: false, // 是否隐藏输入(密码)
autocorrect: true, // 自动纠错
enableSuggestions: true, // 显示建议
// 事件回调
onChanged: (value) {
print('输入内容:$value');
},
onSubmitted: (value) {
print('提交内容:$value');
},
onEditingComplete: () {
print('编辑完成');
},
// 样式
style: TextStyle(fontSize: 16, color: Colors.black),
textAlign: TextAlign.start,
)
完整的登录表单示例:
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// 用户名输入框
TextField(
controller: _usernameController,
decoration: InputDecoration(
labelText: '用户名',
hintText: '请输入用户名',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.text,
),
SizedBox(height: 16),
// 密码输入框
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: '密码',
hintText: '请输入密码',
prefixIcon: Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
border: OutlineInputBorder(),
),
obscureText: _obscurePassword,
),
SizedBox(height: 24),
// 登录按钮
ElevatedButton(
onPressed: () {
String username = _usernameController.text;
String password = _passwordController.text;
print('用户名:$username,密码:$password');
},
child: Text('登录'),
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 50),
),
),
],
),
);
}
}
TextFormField(表单输入框)
定义: TextField 的增强版本,支持表单验证。
基础用法:
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: '邮箱'),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入邮箱';
}
if (!value.contains('@')) {
return '请输入有效的邮箱地址';
}
return null; // 验证通过
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print('验证通过');
}
},
child: Text('提交'),
),
],
),
)
完整的注册表单示例:
class RegisterForm extends StatefulWidget {
@override
_RegisterFormState createState() => _RegisterFormState();
}
class _RegisterFormState extends State<RegisterForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// 邮箱
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: '邮箱',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入邮箱';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return '请输入有效的邮箱地址';
}
return null;
},
),
SizedBox(height: 16),
// 密码
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: '密码',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入密码';
}
if (value.length < 6) {
return '密码至少6个字符';
}
return null;
},
),
SizedBox(height: 16),
// 确认密码
TextFormField(
controller: _confirmPasswordController,
decoration: InputDecoration(
labelText: '确认密码',
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return '请确认密码';
}
if (value != _passwordController.text) {
return '两次密码不一致';
}
return null;
},
),
SizedBox(height: 24),
// 注册按钮
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print('注册成功');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('注册成功')),
);
}
},
child: Text('注册'),
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 50),
),
),
],
),
),
);
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
}
13-1. Form 表单验证
Form + FormField
定义: Form 是表单容器,配合 TextFormField 实现表单验证功能。
完整表单示例:
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// 用户名输入
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
labelText: '用户名',
hintText: '请输入用户名',
prefixIcon: Icon(Icons.person),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入用户名';
}
if (value.length < 3) {
return '用户名至少3个字符';
}
return null; // 验证通过
},
),
SizedBox(height: 16),
// 密码输入
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: '密码',
hintText: '请输入密码',
prefixIcon: Icon(Icons.lock),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入密码';
}
if (value.length < 6) {
return '密码至少6个字符';
}
return null;
},
),
SizedBox(height: 24),
// 登录按钮
ElevatedButton(
onPressed: () {
// 验证所有字段
if (_formKey.currentState!.validate()) {
// 验证通过,执行登录
print('用户名:${_usernameController.text}');
print('密码:${_passwordController.text}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('登录成功')),
);
}
},
child: Text('登录'),
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 50),
),
),
],
),
),
);
}
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
}
自定义验证规则
常用验证规则:
class Validators {
// 必填验证
static String? required(String? value, {String? message}) {
if (value == null || value.isEmpty) {
return message ?? '此字段为必填项';
}
return null;
}
// 邮箱验证
static String? email(String? value) {
if (value == null || value.isEmpty) return '请输入邮箱';
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return '请输入有效的邮箱地址';
}
return null;
}
// 手机号验证
static String? phone(String? value) {
if (value == null || value.isEmpty) return '请输入手机号';
final phoneRegex = RegExp(r'^1[3-9]\d{9}$');
if (!phoneRegex.hasMatch(value)) {
return '请输入有效的手机号';
}
return null;
}
// 长度验证
static String? minLength(String? value, int min) {
if (value == null || value.isEmpty) return '此字段为必填项';
if (value.length < min) {
return '长度不能少于 $min 个字符';
}
return null;
}
// 密码强度验证
static String? password(String? value) {
if (value == null || value.isEmpty) return '请输入密码';
if (value.length < 8) {
return '密码长度不能少于8个字符';
}
if (!RegExp(r'[A-Z]').hasMatch(value)) {
return '密码必须包含大写字母';
}
if (!RegExp(r'[a-z]').hasMatch(value)) {
return '密码必须包含小写字母';
}
if (!RegExp(r'[0-9]').hasMatch(value)) {
return '密码必须包含数字';
}
return null;
}
// 组合验证
static String? combine(String? value, List<String? Function(String?)> validators) {
for (var validator in validators) {
final result = validator(value);
if (result != null) return result;
}
return null;
}
}
// 使用示例
TextFormField(
validator: (value) => Validators.email(value),
)
// 组合验证
TextFormField(
validator: (value) => Validators.combine(value, [
Validators.required,
(v) => Validators.minLength(v, 6),
]),
)
Form 进阶用法
保存表单数据:
class UserForm extends StatefulWidget {
@override
_UserFormState createState() => _UserFormState();
}
class _UserFormState extends State<UserForm> {
final _formKey = GlobalKey<FormState>();
String? _name;
String? _email;
int? _age;
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: '姓名'),
validator: (value) => value?.isEmpty ?? true ? '请输入姓名' : null,
onSaved: (value) => _name = value,
),
TextFormField(
decoration: InputDecoration(labelText: '邮箱'),
validator: Validators.email,
onSaved: (value) => _email = value,
),
TextFormField(
decoration: InputDecoration(labelText: '年龄'),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) return '请输入年龄';
final age = int.tryParse(value);
if (age == null || age < 0 || age > 120) {
return '请输入有效的年龄';
}
return null;
},
onSaved: (value) => _age = int.tryParse(value ?? '0'),
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// 保存所有字段的值
_formKey.currentState!.save();
print('姓名:$_name');
print('邮箱:$_email');
print('年龄:$_age');
}
},
child: Text('提交'),
),
],
),
);
}
}
重置表单:
ElevatedButton(
onPressed: () {
_formKey.currentState!.reset(); // 重置所有字段
},
child: Text('重置'),
)
13-2. 焦点管理 (FocusNode)
FocusNode 基础
定义: 管理输入框的焦点状态,控制键盘显示/隐藏。
基础用法:
class FocusDemo extends StatefulWidget {
@override
_FocusDemoState createState() => _FocusDemoState();
}
class _FocusDemoState extends State<FocusDemo> {
final _usernameFocus = FocusNode();
final _passwordFocus = FocusNode();
@override
void dispose() {
_usernameFocus.dispose();
_passwordFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
focusNode: _usernameFocus,
decoration: InputDecoration(labelText: '用户名'),
textInputAction: TextInputAction.next,
onSubmitted: (_) {
// 回车后跳转到下一个输入框
_passwordFocus.requestFocus();
},
),
TextField(
focusNode: _passwordFocus,
decoration: InputDecoration(labelText: '密码'),
obscureText: true,
textInputAction: TextInputAction.done,
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
_usernameFocus.requestFocus(); // 请求焦点
},
child: Text('聚焦用户名'),
),
ElevatedButton(
onPressed: () {
_usernameFocus.unfocus(); // 失去焦点
_passwordFocus.unfocus();
},
child: Text('失去焦点'),
),
],
),
],
);
}
}
焦点监听
监听焦点变化:
class FocusListenerDemo extends StatefulWidget {
@override
_FocusListenerDemoState createState() => _FocusListenerDemoState();
}
class _FocusListenerDemoState extends State<FocusListenerDemo> {
final _focusNode = FocusNode();
bool _hasFocus = false;
@override
void initState() {
super.initState();
// 添加焦点监听
_focusNode.addListener(() {
setState(() {
_hasFocus = _focusNode.hasFocus;
});
if (_focusNode.hasFocus) {
print('获得焦点');
} else {
print('失去焦点');
}
});
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
focusNode: _focusNode,
decoration: InputDecoration(
labelText: '输入框',
border: OutlineInputBorder(
borderSide: BorderSide(
color: _hasFocus ? Colors.blue : Colors.grey,
width: _hasFocus ? 2 : 1,
),
),
),
),
SizedBox(height: 20),
Text(
_hasFocus ? '输入框已聚焦' : '输入框未聚焦',
style: TextStyle(
color: _hasFocus ? Colors.blue : Colors.grey,
fontSize: 16,
),
),
],
);
}
}
隐藏键盘
多种方式隐藏键盘:
// 方法1:让所有输入框失去焦点
FocusScope.of(context).unfocus();
// 方法2:请求一个空焦点
FocusScope.of(context).requestFocus(FocusNode());
// 方法3:使用 GestureDetector
GestureDetector(
onTap: () {
FocusScope.of(context).unfocus(); // 点击空白处隐藏键盘
},
child: Scaffold(
body: Column(
children: [
TextField(decoration: InputDecoration(labelText: '输入框')),
],
),
),
)
自动聚焦
页面打开时自动聚焦:
class AutoFocusDemo extends StatefulWidget {
@override
_AutoFocusDemoState createState() => _AutoFocusDemoState();
}
class _AutoFocusDemoState extends State<AutoFocusDemo> {
final _focusNode = FocusNode();
@override
void initState() {
super.initState();
// 延迟请求焦点,确保页面已构建完成
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
focusNode: _focusNode,
decoration: InputDecoration(labelText: '自动聚焦的输入框'),
);
}
}
// 或者使用 autofocus 属性
TextField(
autofocus: true, // 更简单的方式
decoration: InputDecoration(labelText: '自动聚焦'),
)
焦点顺序控制
完整的表单焦点流转:
class FormFocusDemo extends StatefulWidget {
@override
_FormFocusDemoState createState() => _FormFocusDemoState();
}
class _FormFocusDemoState extends State<FormFocusDemo> {
final _nameFocus = FocusNode();
final _phoneFocus = FocusNode();
final _emailFocus = FocusNode();
final _addressFocus = FocusNode();
@override
void dispose() {
_nameFocus.dispose();
_phoneFocus.dispose();
_emailFocus.dispose();
_addressFocus.dispose();
super.dispose();
}
void _nextFocus(FocusNode currentFocus, FocusNode nextFocus) {
currentFocus.unfocus();
FocusScope.of(context).requestFocus(nextFocus);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(title: Text('焦点顺序控制')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
TextField(
focusNode: _nameFocus,
decoration: InputDecoration(labelText: '姓名'),
textInputAction: TextInputAction.next,
onSubmitted: (_) => _nextFocus(_nameFocus, _phoneFocus),
),
TextField(
focusNode: _phoneFocus,
decoration: InputDecoration(labelText: '手机号'),
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
onSubmitted: (_) => _nextFocus(_phoneFocus, _emailFocus),
),
TextField(
focusNode: _emailFocus,
decoration: InputDecoration(labelText: '邮箱'),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
onSubmitted: (_) => _nextFocus(_emailFocus, _addressFocus),
),
TextField(
focusNode: _addressFocus,
decoration: InputDecoration(labelText: '地址'),
textInputAction: TextInputAction.done,
onSubmitted: (_) {
_addressFocus.unfocus();
print('提交表单');
},
),
],
),
),
),
);
}
}
14. 按钮组件
ElevatedButton(凸起按钮)
定义: Material Design 风格的凸起按钮,有阴影效果。
基础用法:
ElevatedButton(
onPressed: () {
print('按钮被点击');
},
child: Text('点击我'),
)
// 禁用状态
ElevatedButton(
onPressed: null, // 设置为 null 禁用按钮
child: Text('禁用按钮'),
)
自定义样式:
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, // 背景色
foregroundColor: Colors.white, // 文字颜色
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 5, // 阴影高度
),
child: Text('自定义按钮'),
)
带图标的按钮:
ElevatedButton.icon(
onPressed: () {},
icon: Icon(Icons.send),
label: Text('发送'),
)
TextButton(文本按钮)
定义: 扁平的文本按钮,没有背景和阴影。
基础用法:
TextButton(
onPressed: () {},
child: Text('文本按钮'),
)
// 自定义样式
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: EdgeInsets.all(16),
),
child: Text('自定义文本按钮'),
)
OutlinedButton(轮廓按钮)
定义: 带边框的按钮,没有背景色。
基础用法:
OutlinedButton(
onPressed: () {},
child: Text('轮廓按钮'),
)
// 自定义样式
OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
side: BorderSide(color: Colors.blue, width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text('自定义轮廓按钮'),
)
IconButton(图标按钮)
定义: 只包含图标的圆形按钮。
基础用法:
IconButton(
icon: Icon(Icons.favorite),
onPressed: () {},
color: Colors.red,
iconSize: 30,
tooltip: '收藏', // 长按显示的提示
)
FloatingActionButton(悬浮按钮)
定义: 悬浮在页面上的圆形按钮,通常用于主要操作。
基础用法:
FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
tooltip: '添加',
)
// 扩展样式
FloatingActionButton.extended(
onPressed: () {},
icon: Icon(Icons.add),
label: Text('添加项目'),
)
15. Scaffold 脚手架
Scaffold(页面脚手架)
定义: Material Design 的基本页面布局结构,提供 AppBar、Drawer、BottomNavigationBar 等。
完整示例:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
// 顶部导航栏
appBar: AppBar(
title: Text('首页'),
actions: [
IconButton(icon: Icon(Icons.search), onPressed: () {}),
IconButton(icon: Icon(Icons.more_vert), onPressed: () {}),
],
),
// 主体内容
body: Center(
child: Text('页面内容'),
),
// 悬浮按钮
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
// 悬浮按钮位置
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
// 左侧抽屉菜单
drawer: Drawer(
child: ListView(
children: [
DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text('菜单', style: TextStyle(color: Colors.white, fontSize: 24)),
),
ListTile(
leading: Icon(Icons.home),
title: Text('首页'),
onTap: () {},
),
ListTile(
leading: Icon(Icons.settings),
title: Text('设置'),
onTap: () {},
),
],
),
),
// 底部导航栏
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.business), label: '商城'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
currentIndex: 0,
onTap: (index) {
print('点击了第 $index 项');
},
),
);
}
}
BottomNavigationBar 完整应用示例
定义: 实现底部导航栏的多页面切换,配合 IndexedStack 保持页面状态。
完整示例:
class MainPage extends StatefulWidget {
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
// 页面列表
final List<Widget> _pages = [
HomePage(),
ShopPage(),
ProfilePage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex, // 使用 IndexedStack 保持页面状态
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
activeIcon: Icon(Icons.home, color: Colors.blue),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart),
activeIcon: Icon(Icons.shopping_cart, color: Colors.blue),
label: '商城',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
activeIcon: Icon(Icons.person, color: Colors.blue),
label: '我的',
),
],
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.grey,
type: BottomNavigationBarType.fixed, // 固定类型,超过3个item时推荐
),
);
}
}
// 首页
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin {
int _counter = 0;
@override
bool get wantKeepAlive => true; // 保持状态
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用
return Scaffold(
appBar: AppBar(title: Text('首页')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('首页计数:$_counter', style: TextStyle(fontSize: 24)),
ElevatedButton(
onPressed: () {
setState(() {
_counter++;
});
},
child: Text('增加'),
),
],
),
),
);
}
}
// 商城页面
class ShopPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('商城')),
body: Center(child: Text('商城页面')),
);
}
}
// 我的页面
class ProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('我的')),
body: Center(child: Text('个人中心')),
);
}
}
IndexedStack vs PageView:
// IndexedStack:所有页面都会保持状态,切换时不重建
IndexedStack(
index: _currentIndex,
children: _pages,
)
// PageView:可以滑动切换,但默认不保持状态
PageView(
controller: _pageController,
children: _pages,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
)
SafeArea(安全区域)
定义: 自动适配刘海屏、状态栏、导航条等系统UI,确保内容不被遮挡。
基础用法:
Scaffold(
body: SafeArea(
child: Column(
children: [
Text('这段内容不会被刘海屏遮挡'),
// ... 其他内容
],
),
),
)
指定安全区域位置:
SafeArea(
top: true, // 顶部安全区域
bottom: true, // 底部安全区域
left: true, // 左侧安全区域
right: true, // 右侧安全区域
child: Container(
color: Colors.blue,
child: Center(child: Text('内容')),
),
)
部分区域不使用安全区域:
SafeArea(
top: false, // 顶部不留空白,例如全屏图片
bottom: true,
child: Column(
children: [
Container(
height: 200,
color: Colors.blue,
child: Center(child: Text('全屏顶部图片')),
),
Expanded(
child: Center(child: Text('其他内容')),
),
],
),
)
SafeArea + Scaffold 组合:
// 推荐做法:在 Scaffold 内使用 SafeArea
Scaffold(
appBar: AppBar(title: Text('标题')),
body: SafeArea(
child: Column(
children: [
// 内容不会被系统UI遮挡
],
),
),
)
AppBar(顶部导航栏)
详细用法:
AppBar(
// 标题
title: Text('标题'),
// 左侧按钮(默认是返回按钮)
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {},
),
// 右侧按钮组
actions: [
IconButton(icon: Icon(Icons.search), onPressed: () {}),
IconButton(icon: Icon(Icons.shopping_cart), onPressed: () {}),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(child: Text('设置'), value: '设置'),
PopupMenuItem(child: Text('关于'), value: '关于'),
],
onSelected: (value) {
print('选择了:$value');
},
),
],
// 背景色
backgroundColor: Colors.blue,
// 阴影高度
elevation: 4,
// 底部 Widget
bottom: TabBar(
tabs: [
Tab(text: '推荐'),
Tab(text: '热门'),
Tab(text: '最新'),
],
),
)
16. 对话框与提示
AlertDialog(警告对话框)
基础用法:
void _showDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('提示'),
content: Text('确定要删除吗?'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context); // 关闭对话框
},
child: Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
print('确认删除');
},
child: Text('确定'),
),
],
);
},
);
}
// 使用
ElevatedButton(
onPressed: () => _showDialog(context),
child: Text('显示对话框'),
)
自定义对话框:
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Row(
children: [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 10),
Text('警告'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('这是一个自定义对话框'),
SizedBox(height: 10),
TextField(
decoration: InputDecoration(
hintText: '请输入内容',
border: OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text('确定'),
),
],
);
},
);
SnackBar(底部提示条)
基础用法:
void _showSnackBar(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('操作成功'),
duration: Duration(seconds: 2),
),
);
}
// 使用
ElevatedButton(
onPressed: () => _showSnackBar(context),
child: Text('显示提示'),
)
带操作的 SnackBar:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已删除'),
action: SnackBarAction(
label: '撤销',
onPressed: () {
print('撤销删除');
},
),
duration: Duration(seconds: 3),
backgroundColor: Colors.red,
),
);
BottomSheet(底部弹窗)
模态底部弹窗:
void _showBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) {
return Container(
height: 200,
child: Column(
children: [
ListTile(
leading: Icon(Icons.photo),
title: Text('拍照'),
onTap: () {
Navigator.pop(context);
print('选择了拍照');
},
),
ListTile(
leading: Icon(Icons.image),
title: Text('从相册选择'),
onTap: () {
Navigator.pop(context);
print('选择了相册');
},
),
ListTile(
leading: Icon(Icons.cancel),
title: Text('取消'),
onTap: () => Navigator.pop(context),
),
],
),
);
},
);
}
DatePicker(日期选择器)
基础用法:
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
print('选择的日期:$picked');
}
}
TimePicker(时间选择器)
基础用法:
Future<void> _selectTime(BuildContext context) async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
print('选择的时间:${picked.hour}:${picked.minute}');
}
}
17. 交互组件
Switch(开关)
基础用法:
class SwitchDemo extends StatefulWidget {
@override
_SwitchDemoState createState() => _SwitchDemoState();
}
class _SwitchDemoState extends State<SwitchDemo> {
bool _switchValue = false;
@override
Widget build(BuildContext context) {
return Switch(
value: _switchValue,
onChanged: (value) {
setState(() {
_switchValue = value;
});
},
activeColor: Colors.blue,
);
}
}
Checkbox(复选框)
基础用法:
class CheckboxDemo extends StatefulWidget {
@override
_CheckboxDemoState createState() => _CheckboxDemoState();
}
class _CheckboxDemoState extends State<CheckboxDemo> {
bool _checked = false;
@override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text('同意用户协议'),
value: _checked,
onChanged: (value) {
setState(() {
_checked = value!;
});
},
controlAffinity: ListTileControlAffinity.leading, // 复选框在左侧
);
}
}
Radio(单选框)
基础用法:
class RadioDemo extends StatefulWidget {
@override
_RadioDemoState createState() => _RadioDemoState();
}
class _RadioDemoState extends State<RadioDemo> {
String _selectedValue = '选项1';
@override
Widget build(BuildContext context) {
return Column(
children: [
RadioListTile<String>(
title: Text('选项1'),
value: '选项1',
groupValue: _selectedValue,
onChanged: (value) {
setState(() {
_selectedValue = value!;
});
},
),
RadioListTile<String>(
title: Text('选项2'),
value: '选项2',
groupValue: _selectedValue,
onChanged: (value) {
setState(() {
_selectedValue = value!;
});
},
),
],
);
}
}
Slider(滑块)
基础用法:
class SliderDemo extends StatefulWidget {
@override
_SliderDemoState createState() => _SliderDemoState();
}
class _SliderDemoState extends State<SliderDemo> {
double _sliderValue = 50;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('当前值:${_sliderValue.toInt()}'),
Slider(
value: _sliderValue,
min: 0,
max: 100,
divisions: 10, // 分段数
label: _sliderValue.toInt().toString(),
onChanged: (value) {
setState(() {
_sliderValue = value;
});
},
),
],
);
}
}
18. 手势检测
GestureDetector(手势检测器)
定义: 检测各种手势操作,如点击、双击、长按、拖动等。
基础用法:
GestureDetector(
onTap: () {
print('单击');
},
onDoubleTap: () {
print('双击');
},
onLongPress: () {
print('长按');
},
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: Center(child: Text('点击我')),
),
)
拖动示例:
class DraggableDemo extends StatefulWidget {
@override
_DraggableDemoState createState() => _DraggableDemoState();
}
class _DraggableDemoState extends State<DraggableDemo> {
double _top = 100;
double _left = 100;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
top: _top,
left: _left,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_left += details.delta.dx;
_top += details.delta.dy;
});
},
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text('拖动我')),
),
),
),
],
);
}
}
InkWell(水波纹点击)
定义: 带水波纹效果的点击组件。
基础用法:
InkWell(
onTap: () {
print('点击');
},
child: Container(
padding: EdgeInsets.all(16),
child: Text('点击我会有水波纹效果'),
),
)
19. 异步处理
Future 与 async/await
基础用法:
// 异步函数
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2)); // 模拟网络延迟
return '获取的数据';
}
// 使用
void loadData() async {
print('开始加载');
String data = await fetchData();
print('数据:$data');
}
FutureBuilder(异步构建器)
定义: 根据 Future 的状态自动构建 UI。
基础用法:
class FutureBuilderDemo extends StatelessWidget {
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return '加载完成的数据';
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: fetchData(),
builder: (context, snapshot) {
// 加载中
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
// 加载失败
if (snapshot.hasError) {
return Center(child: Text('错误:${snapshot.error}'));
}
// 加载成功
if (snapshot.hasData) {
return Center(child: Text('数据:${snapshot.data}'));
}
return Center(child: Text('无数据'));
},
);
}
}
StreamBuilder(流构建器)
定义: 根据 Stream 的数据流自动构建 UI。
基础用法:
class StreamBuilderDemo extends StatelessWidget {
Stream<int> counterStream() async* {
for (int i = 0; i < 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: counterStream(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('计数:${snapshot.data}', style: TextStyle(fontSize: 48));
}
return CircularProgressIndicator();
},
);
}
}
20. TabBar 导航
TabBar & TabBarView
定义: 实现顶部或底部标签页导航。
完整示例:
class TabBarDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3, // 标签数量
child: Scaffold(
appBar: AppBar(
title: Text('TabBar 示例'),
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.home), text: '首页'),
Tab(icon: Icon(Icons.business), text: '商城'),
Tab(icon: Icon(Icons.person), text: '我的'),
],
),
),
body: TabBarView(
children: [
Center(child: Text('首页内容', style: TextStyle(fontSize: 24))),
Center(child: Text('商城内容', style: TextStyle(fontSize: 24))),
Center(child: Text('我的内容', style: TextStyle(fontSize: 24))),
],
),
),
);
}
}
使用 TabController:
class TabBarControllerDemo extends StatefulWidget {
@override
_TabBarControllerDemoState createState() => _TabBarControllerDemoState();
}
class _TabBarControllerDemoState extends State<TabBarControllerDemo>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
// 监听标签切换
_tabController.addListener(() {
if (!_tabController.indexIsChanging) {
print('切换到标签 ${_tabController.index}');
}
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TabController 示例'),
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(text: '推荐'),
Tab(text: '热门'),
Tab(text: '最新'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
ListView.builder(
itemCount: 20,
itemBuilder: (context, index) => ListTile(title: Text('推荐 $index')),
),
ListView.builder(
itemCount: 20,
itemBuilder: (context, index) => ListTile(title: Text('热门 $index')),
),
ListView.builder(
itemCount: 20,
itemBuilder: (context, index) => ListTile(title: Text('最新 $index')),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 程序控制切换标签
_tabController.animateTo((_tabController.index + 1) % 3);
},
child: Icon(Icons.navigate_next),
),
);
}
}
21. 卡片与列表
Card(卡片组件)
定义: Material Design 风格的卡片,带圆角和阴影。
基础用法:
Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('卡片标题', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('这是卡片内容...'),
],
),
),
)
自定义卡片:
Card(
elevation: 8, // 阴影高度
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: EdgeInsets.all(16),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: Image.network(
'https://picsum.photos/400/200',
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
),
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('卡片标题', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('这是一张带图片的卡片示例'),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(onPressed: () {}, child: Text('取消')),
SizedBox(width: 8),
ElevatedButton(onPressed: () {}, child: Text('确定')),
],
),
],
),
),
],
),
)
列表中的卡片:
ListView.builder(
padding: EdgeInsets.all(8),
itemCount: 10,
itemBuilder: (context, index) {
return Card(
margin: EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
),
title: Text('项目 ${index + 1}'),
subtitle: Text('这是项目的描述信息'),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () {
print('点击了项目 $index');
},
),
);
},
)
ExpansionTile(可展开列表)
定义: 可以展开和收起的列表项。
基础用法:
ExpansionTile(
leading: Icon(Icons.folder),
title: Text('可展开项'),
subtitle: Text('点击展开查看更多'),
children: [
ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text('子项 1'),
onTap: () {},
),
ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text('子项 2'),
onTap: () {},
),
ListTile(
leading: Icon(Icons.insert_drive_file),
title: Text('子项 3'),
onTap: () {},
),
],
)
实际应用 - 分类菜单:
class CategoryMenu extends StatelessWidget {
final List<Map<String, dynamic>> categories = [
{
'title': '电子产品',
'icon': Icons.devices,
'items': ['手机', '电脑', '平板']
},
{
'title': '服装鞋帽',
'icon': Icons.shopping_bag,
'items': ['男装', '女装', '童装']
},
{
'title': '图书音像',
'icon': Icons.book,
'items': ['文学', '科技', '教育']
},
];
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
return Card(
child: ExpansionTile(
leading: Icon(category['icon']),
title: Text(category['title']),
children: (category['items'] as List<String>).map((item) {
return ListTile(
contentPadding: EdgeInsets.only(left: 72, right: 16),
title: Text(item),
onTap: () {
print('选择了:${category['title']} - $item');
},
);
}).toList(),
),
);
},
);
}
}
22. 选择器组件
DropdownButton(下拉选择框)
定义: 下拉选择菜单。
基础用法:
class DropdownDemo extends StatefulWidget {
@override
_DropdownDemoState createState() => _DropdownDemoState();
}
class _DropdownDemoState extends State<DropdownDemo> {
String _selectedValue = '选项1';
@override
Widget build(BuildContext context) {
return DropdownButton<String>(
value: _selectedValue,
items: ['选项1', '选项2', '选项3', '选项4'].map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_selectedValue = newValue!;
});
},
);
}
}
完整示例 - 城市选择:
class CitySelector extends StatefulWidget {
@override
_CitySelectorState createState() => _CitySelectorState();
}
class _CitySelectorState extends State<CitySelector> {
String _selectedCity = '北京';
final List<String> _cities = [
'北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安'
];
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedCity,
isExpanded: true,
icon: Icon(Icons.arrow_drop_down),
items: _cities.map((String city) {
return DropdownMenuItem<String>(
value: city,
child: Text(city),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_selectedCity = newValue!;
});
print('选择了城市:$_selectedCity');
},
),
),
);
}
}
Chip 系列(标签芯片)
1. Chip(基础芯片):
Chip(
label: Text('标签'),
avatar: CircleAvatar(child: Text('A')),
deleteIcon: Icon(Icons.close),
onDeleted: () {
print('删除标签');
},
)
2. ChoiceChip(选择芯片):
class ChoiceChipDemo extends StatefulWidget {
@override
_ChoiceChipDemoState createState() => _ChoiceChipDemoState();
}
class _ChoiceChipDemoState extends State<ChoiceChipDemo> {
int _selectedIndex = 0;
final List<String> _options = ['全部', '待付款', '待发货', '待收货', '已完成'];
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
children: List.generate(_options.length, (index) {
return ChoiceChip(
label: Text(_options[index]),
selected: _selectedIndex == index,
onSelected: (selected) {
setState(() {
_selectedIndex = index;
});
},
);
}),
);
}
}
3. FilterChip(过滤芯片):
class FilterChipDemo extends StatefulWidget {
@override
_FilterChipDemoState createState() => _FilterChipDemoState();
}
class _FilterChipDemoState extends State<FilterChipDemo> {
List<String> _selectedFilters = [];
final List<String> _filters = ['价格', '品牌', '评分', '销量', '折扣'];
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
children: _filters.map((filter) {
return FilterChip(
label: Text(filter),
selected: _selectedFilters.contains(filter),
onSelected: (selected) {
setState(() {
if (selected) {
_selectedFilters.add(filter);
} else {
_selectedFilters.remove(filter);
}
});
print('已选择:$_selectedFilters');
},
);
}).toList(),
);
}
}
23. 进度与加载
CircularProgressIndicator(圆形进度条)
基础用法:
// 不确定进度
CircularProgressIndicator()
// 确定进度
CircularProgressIndicator(
value: 0.6, // 0.0 到 1.0
strokeWidth: 6,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
)
实际应用:
class LoadingDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 20),
Text('加载中...', style: TextStyle(fontSize: 16)),
],
),
);
}
}
LinearProgressIndicator(线性进度条)
基础用法:
// 不确定进度
LinearProgressIndicator()
// 确定进度
LinearProgressIndicator(
value: 0.7, // 0.0 到 1.0
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
)
下载进度示例:
class DownloadProgress extends StatefulWidget {
@override
_DownloadProgressState createState() => _DownloadProgressState();
}
class _DownloadProgressState extends State<DownloadProgress> {
double _progress = 0.0;
Timer? _timer;
@override
void initState() {
super.initState();
_startDownload();
}
void _startDownload() {
_timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
setState(() {
_progress += 0.01;
if (_progress >= 1.0) {
_progress = 1.0;
timer.cancel();
}
});
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
LinearProgressIndicator(value: _progress),
SizedBox(height: 20),
Text('${(_progress * 100).toInt()}%', style: TextStyle(fontSize: 24)),
],
);
}
}
RefreshIndicator(下拉刷新)
定义: 为可滚动组件添加下拉刷新功能。
基础用法:
class RefreshDemo extends StatefulWidget {
@override
_RefreshDemoState createState() => _RefreshDemoState();
}
class _RefreshDemoState extends State<RefreshDemo> {
List<String> _items = List.generate(20, (index) => '项目 $index');
Future<void> _onRefresh() async {
// 模拟网络请求
await Future.delayed(Duration(seconds: 2));
setState(() {
_items = List.generate(20, (index) => '刷新后的项目 $index');
});
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_items[index]),
);
},
),
);
}
}
24. 滑动操作
Dismissible(滑动删除)
定义: 通过滑动手势删除列表项。
基础用法:
class DismissibleDemo extends StatefulWidget {
@override
_DismissibleDemoState createState() => _DismissibleDemoState();
}
class _DismissibleDemoState extends State<DismissibleDemo> {
List<String> _items = List.generate(20, (index) => '项目 $index');
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
final item = _items[index];
return Dismissible(
key: Key(item),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20),
child: Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart, // 只能从右向左滑动
onDismissed: (direction) {
setState(() {
_items.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已删除 $item'),
action: SnackBarAction(
label: '撤销',
onPressed: () {
setState(() {
_items.insert(index, item);
});
},
),
),
);
},
confirmDismiss: (direction) async {
// 确认删除对话框
return await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('确认删除'),
content: Text('确定要删除 $item 吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('确定'),
),
],
);
},
);
},
child: ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(item),
subtitle: Text('向左滑动删除'),
),
);
},
);
}
}
双向滑动不同操作:
Dismissible(
key: Key(item),
// 向右滑动的背景(归档)
background: Container(
color: Colors.green,
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 20),
child: Icon(Icons.archive, color: Colors.white),
),
// 向左滑动的背景(删除)
secondaryBackground: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20),
child: Icon(Icons.delete, color: Colors.white),
),
onDismissed: (direction) {
if (direction == DismissDirection.startToEnd) {
print('归档:$item');
} else {
print('删除:$item');
}
},
child: ListTile(title: Text(item)),
)
25. 主题与适配
ThemeData(主题配置)
定义: 配置应用的全局主题样式。
基础配置:
MaterialApp(
theme: ThemeData(
// 主色调
primarySwatch: Colors.blue,
primaryColor: Colors.blue,
// 强调色
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
// 文本主题
textTheme: TextTheme(
displayLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
bodyLarge: TextStyle(fontSize: 16),
bodyMedium: TextStyle(fontSize: 14),
),
// 按钮主题
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
// AppBar 主题
appBarTheme: AppBarTheme(
elevation: 0,
centerTitle: true,
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
// 卡片主题
cardTheme: CardTheme(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
home: HomePage(),
)
深色模式:
class ThemeDemo extends StatefulWidget {
@override
_ThemeDemoState createState() => _ThemeDemoState();
}
class _ThemeDemoState extends State<ThemeDemo> {
bool _isDarkMode = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.blue,
),
darkTheme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.blue,
),
themeMode: _isDarkMode ? ThemeMode.dark : ThemeMode.light,
home: Scaffold(
appBar: AppBar(title: Text('主题切换')),
body: Center(
child: Switch(
value: _isDarkMode,
onChanged: (value) {
setState(() {
_isDarkMode = value;
});
},
),
),
),
);
}
}
MediaQuery(屏幕适配)
定义: 获取设备屏幕信息,实现响应式布局。
获取屏幕信息:
@override
Widget build(BuildContext context) {
// 获取屏幕尺寸
final size = MediaQuery.of(context).size;
final width = size.width;
final height = size.height;
// 获取设备像素比
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
// 获取状态栏高度
final padding = MediaQuery.of(context).padding;
final statusBarHeight = padding.top;
// 获取系统亮度模式
final brightness = MediaQuery.of(context).platformBrightness;
final isDark = brightness == Brightness.dark;
return Container(
width: width * 0.8, // 屏幕宽度的 80%
height: height * 0.5, // 屏幕高度的 50%
child: Text('屏幕宽度:$width\n屏幕高度:$height'),
);
}
响应式布局示例:
class ResponsiveLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final width = size.width;
// 根据屏幕宽度判断布局
if (width < 600) {
// 手机布局
return MobileLayout();
} else if (width < 900) {
// 平板布局
return TabletLayout();
} else {
// 桌面布局
return DesktopLayout();
}
}
}
class MobileLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('手机布局', style: TextStyle(fontSize: 24)),
// 单列布局
],
);
}
}
LayoutBuilder(布局构建器):
LayoutBuilder(
builder: (context, constraints) {
// constraints.maxWidth 可用的最大宽度
// constraints.maxHeight 可用的最大高度
if (constraints.maxWidth > 600) {
return Row(
children: [
Expanded(child: Text('左侧')),
Expanded(child: Text('右侧')),
],
);
} else {
return Column(
children: [
Text('顶部'),
Text('底部'),
],
);
}
},
)
26. 其他实用组件
Tooltip(提示文本)
基础用法:
Tooltip(
message: '这是一个提示',
child: IconButton(
icon: Icon(Icons.info),
onPressed: () {},
),
)
PopupMenuButton(弹出菜单)
详细用法:
PopupMenuButton<String>(
icon: Icon(Icons.more_vert),
itemBuilder: (context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 20),
SizedBox(width: 10),
Text('编辑'),
],
),
),
PopupMenuItem(
value: 'share',
child: Row(
children: [
Icon(Icons.share, size: 20),
SizedBox(width: 10),
Text('分享'),
],
),
),
PopupMenuDivider(),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, size: 20, color: Colors.red),
SizedBox(width: 10),
Text('删除', style: TextStyle(color: Colors.red)),
],
),
),
],
onSelected: (value) {
print('选择了:$value');
},
)
六、滚动组件
14. 基础滚动组件
SingleChildScrollView(单子组件滚动)
定义: 当内容超出屏幕时,提供滚动功能的容器,只能包含一个子组件。
基础用法:
SingleChildScrollView(
child: Column(
children: [
Container(height: 200, color: Colors.red),
Container(height: 200, color: Colors.green),
Container(height: 200, color: Colors.blue),
],
),
)
常用属性:
SingleChildScrollView(
scrollDirection: Axis.vertical, // 滚动方向
reverse: false, // 是否反向滚动
padding: EdgeInsets.all(16), // 内边距
physics: BouncingScrollPhysics(), // 滚动物理效果
child: Column(children: [...]),
)
15. 列表组件
ListView(列表视图)
1. ListView.builder(构建器模式):
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('标题 $index'),
subtitle: Text('副标题 $index'),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () => print('点击了第 $index 项'),
);
},
)
2. ListView.separated(带分隔线):
ListView.separated(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(title: Text('项目 $index'));
},
separatorBuilder: (context, index) {
return Divider(height: 1, color: Colors.grey[300]);
},
)
16. 网格组件
GridView(网格视图)
1. GridView.count(固定列数):
GridView.count(
crossAxisCount: 3, // 每行3列
crossAxisSpacing: 10, // 水平间距
mainAxisSpacing: 10, // 垂直间距
children: List.generate(20, (index) {
return Container(
color: Colors.blue,
child: Center(child: Text('$index')),
);
}),
)
2. GridView.builder(构建器模式):
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 1.0, // 宽高比
),
itemCount: 50,
itemBuilder: (context, index) {
return Card(
child: Center(child: Text('$index')),
);
},
)
17. 自定义滚动容器
CustomScrollView
定义: 使用 Sliver 组件创建更灵活的滚动效果。
基础用法:
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text('标题'),
background: Image.network(url, fit: BoxFit.cover),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('项 $index')),
childCount: 20,
),
),
],
)
18. 整页滚动容器
PageView(页面滚动)
基础用法:
PageView(
children: [
Container(color: Colors.red),
Container(color: Colors.green),
Container(color: Colors.blue),
],
)
PageView.builder:
PageView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return Container(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(child: Text('页面 ${index + 1}')),
);
},
)
七、高级特性
19. 组件通信
1. 父子组件通信
父组件向子组件传递数据(通过构造函数):
// 子组件
class ChildWidget extends StatelessWidget {
final String title;
final int count;
const ChildWidget({
super.key,
required this.title,
required this.count,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('标题:$title'),
Text('数量:$count'),
],
);
}
}
// 父组件使用
ChildWidget(title: '商品', count: 10)
2. 子组件向父组件通信(回调函数)
// 子组件
class CounterButton extends StatelessWidget {
final Function(int) onCountChanged;
const CounterButton({super.key, required this.onCountChanged});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => onCountChanged(1),
child: Text('增加'),
);
}
}
// 父组件
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('计数:$_count'),
CounterButton(
onCountChanged: (value) {
setState(() => _count += value);
},
),
],
);
}
}
3. Provider 状态管理(推荐)
安装:
flutter pub add provider
基础用法:
import 'package:provider/provider.dart';
// 1. 创建数据模型
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // 通知更新
}
}
// 2. 提供 Provider
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MyApp(),
),
);
}
// 3. 使用数据
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 监听数据
Consumer<CounterModel>(
builder: (context, counter, child) {
return Text('计数:${counter.count}');
},
),
ElevatedButton(
onPressed: () {
// 修改数据
context.read<CounterModel>().increment();
},
child: Text('增加'),
),
],
),
),
);
}
}
八、网络与数据
20. 网络请求
Dio 工具
安装:
flutter pub add dio
基础用法:
import 'package:dio/dio.dart';
// GET 请求
Future<void> fetchData() async {
try {
final dio = Dio();
final response = await dio.get('https://api.example.com/data');
print('数据:${response.data}');
} catch (e) {
print('错误:$e');
}
}
// POST 请求
Future<void> postData() async {
final dio = Dio();
final response = await dio.post(
'https://api.example.com/login',
data: {'username': 'admin', 'password': '123456'},
);
print(response.data);
}
封装 HTTP 工具类
class HttpUtil {
static final HttpUtil _instance = HttpUtil._internal();
factory HttpUtil() => _instance;
late Dio dio;
HttpUtil._internal() {
dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
));
// 添加拦截器
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
print('请求:${options.uri}');
options.headers['Authorization'] = 'Bearer token';
handler.next(options);
},
onResponse: (response, handler) {
print('响应:${response.data}');
handler.next(response);
},
onError: (error, handler) {
print('错误:${error.message}');
handler.next(error);
},
));
}
Future<Response> get(String path) async {
return await dio.get(path);
}
Future<Response> post(String path, {dynamic data}) async {
return await dio.post(path, data: data);
}
}
// 使用
void main() async {
final http = HttpUtil();
final response = await http.get('/users');
print(response.data);
}
21. Flutter Web 跨域问题
解决方案
方法一:后端配置 CORS
// 服务端需要添加 CORS 头
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
方法二:使用代理(开发环境)
在项目根目录创建 web/manifest.json,然后使用代理服务器。
方法三:Dio 配置
final dio = Dio();
dio.options.headers['Access-Control-Allow-Origin'] = '*';
总结
本笔记涵盖了 Flutter 的核心知识点:
- 布局组件:Container、Column、Row、Stack、Wrap 等
- 基础组件:Text、Image、TextField 等
- 滚动组件:ListView、GridView、PageView 等
- 状态管理:Provider 等
- 网络请求:Dio 工具封装