Container
Container 是 Flutter 中最常用、最强大的布局组件之一,它是一个单子组件容器,可以用来控制子组件的大小、边距、填充、装饰(背景、边框、阴影、圆角等)、对齐、变换等属性。
简单来说:Container = 装饰(Decoration) + Padding + Margin + 固定宽高/约束 + 对齐 + 变换 的组合。
核心作用
- 当你想给一个 Widget 加背景色、圆角、边框、阴影 时 → 用 Container。
- 当你想控制一个 Widget 的 宽高、内外边距、对齐方式 时 → 用 Container。
- 当你想居中、偏移、旋转某个子组件时 → 用 Container。
常用参数一览
| 参数 | 作用 | 示例值/说明 |
|---|---|---|
child | 容器内的唯一子组件 | 任何 Widget(如 Text、Image、Column) |
width / height | 固定宽度/高度 | 100.0、double.infinity |
padding | 内边距(内容与容器边框之间的距离) | EdgeInsets.all(16) |
margin | 外边距(容器与其他组件的距离) | EdgeInsets.symmetric(horizontal: 20) |
decoration | 装饰:背景色、边框、圆角、阴影、渐变等 | BoxDecoration(...) |
color | 背景色(简便写法,不能与 decoration 同时用) | Colors.blue |
alignment | 子组件在容器内的对齐方式 | Alignment.center |
constraints | 额外尺寸约束 | BoxConstraints(maxWidth: 200) |
transform | 变换(旋转、缩放、平移) | Matrix4.rotationZ(0.1) |
常见使用示例
Container(
padding: EdgeInsets.all(16),
margin: EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.white, // 背景色
// 渐变背景色
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFFEF4D7), Color(0xFFFFFFFF)],
stops: [0.0, 1.0],
),
// 圆角
borderRadius: BorderRadius.circular(10),
// 边框
border: Border.all(color: Color(0xFFD5D5D5), width: 1),
// 阴影
boxShadow: [
BoxShadow(
color: Color(0xFFD6D6D6),
offset: Offset(0, 10),
blurRadius: 0,
spreadRadius: 0,
),
],
),
child: Text('这是一个卡片'),
)
注意事项
- color 和 decoration 不能同时使用: 如果你用了 decoration,背景色要写在 BoxDecoration(color: ...) 里。
- 没有 child 时:Container 会尽量收缩(除非指定宽高)。
- 性能:Container 本身很轻量,但嵌套太多层时建议用 Padding/Margin/DecoratedBox 替代部分功能以优化。
与其他组件的关系(替代方案)
| 需求 | 可以用 Container | 更轻量的替代方案 |
|---|---|---|
| 只有内边距 | 可以 | 用 Padding |
| 只有外边距 | 可以 | 用 Margin(或 SizedBox) |
| 只有背景/边框/圆角 | 可以 | 用 DecoratedBox |
| 居中子组件 | 可以 | 用 Center |
文本
文本展示
Text(
'文本内容',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xff333333),
),
),
限制行数:增加2个参数即可
maxLines: 1,
overflow: TextOverflow.ellipsis,
RichText 和 Text.rich
显示富文本(即一段文字中包含多种样式,如不同颜色、字体、大小、粗体等)
它们的区别仅在于使用方式:
| 项目 | RichText | Text.rich |
|---|---|---|
| 类型 | 独立的 Widget | Text Widget 的命名构造函数 |
| 使用方式 | 直接作为 Widget 使用,需要传入 TextSpan | 通过 Text Widget 的静态方法传入 TextSpan |
| 代码风格 | 更“底层”、更明确(常用于复杂自定义布局) | 更简洁、更符合 Text 的语义(推荐日常使用) |
| 性能 | 完全相同 | 完全相同 |
| 适用场景 | 需要与其它 Inline Widget(如 WidgetSpan)混合布局时 | 纯文本多样式显示(最常见场景) |
RichText
RichText(
text: TextSpan(
style: TextStyle(color: Colors.black, fontSize: 16),
children: [
TextSpan(text: '你好,'),
TextSpan(text: '世界', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
TextSpan(text: '!'),
],
),
),
Text.rich
Text.rich(
TextSpan(
style: TextStyle(color: Colors.black, fontSize: 16),
children: [
TextSpan(text: '你好,'),
TextSpan(text: '世界', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
TextSpan(text: '!'),
],
),
),
从 Flutter 1.20 开始,官方文档更倾向于推荐使用 Text.rich,因为:
- 语义更清晰:你明明是在写一个“Text”,为什么不用 Text 呢?
- 代码更一致:和其他 Text 构造函数(如 Text('普通文字'))风格统一。
- 更易读:一眼看出这是个 Text Widget。
什么时候必须用 RichText?
只有在极少数情况下,你需要将富文本与其他 inline-level 的 Widget 混合(如图片、自定义小部件)时,才必须用 RichText + WidgetSpan:
RichText(
text: TextSpan(
children: [
TextSpan(text: '文字 '),
WidgetSpan(child: Icon(Icons.star, size: 18, color: Colors.amber)),
TextSpan(text: ' 更多文字'),
],
),
),
注意:Text.rich 不支持 WidgetSpan,所以这种场景只能用 RichText。
总结
- 日常 99% 的富文本场景:用 Text.rich 更简洁、推荐。
- 需要嵌入图片、Icon、小部件等:必须用 RichText + WidgetSpan。
- 两者性能无差别,效果完全一致。
推荐:以后写多样式文本,直接用 Text.rich 就行了!
补充
如果需要让文本有点击功能可以这样做:
1.引入
import 'package:flutter/gestures.dart';
2.实现点击功能
TextSpan(
text: '去编辑',
style: TextStyle(color: Color(0xff4A80F0)),
recognizer: TapGestureRecognizer()
..onTap = () async {},
),
flex布局(Row、Column、Wrap)
Flutter 中 Row、Column 和 Wrap 都是用来排列多个子组件(children)的布局 Widget,它们都属于 Flex 布局家族,但适用场景和行为有明显区别。
| Widget | 排列方向 | 主轴(Main Axis) | 交叉轴(Cross Axis) | 当子组件超出空间时的行为 | 典型使用场景 |
|---|---|---|---|---|---|
| Row | 水平(从左到右) | 水平方向 | 垂直方向 | 不自动换行,超出屏幕会报溢出错误(黄色警告条) | 顶部导航栏、按钮组、水平列表项 |
| Column | 垂直(从上到下) | 垂直方向 | 水平方向 | 不自动换行,超出屏幕会报溢出错误 | 页面主体内容、表单、垂直列表项 |
| Wrap | 水平开始,空间不够自动换行 | 水平方向(run) | 垂直方向(spacing) | 自动换行,子组件太多会向下延伸,不会溢出 | 标签云、表情包选择、芯片(Chip)组 |
核心区别总结
- Row 和 Column:不换行,严格单行/单列布局,适合固定数量的子组件。
- Wrap:会自动换行,像“流式布局”,适合子组件数量不确定或动态的情况。
Row
水平排列,不换行(如果子组件太多,会溢出报错(屏幕放不下))
Row(
children: List.generate(
3,
(i) => Container(
width: 80,
height: 80,
color: Colors.blue[100 * (i % 9)],
margin: EdgeInsets.all(4),
child: Center(child: Text('$i')),
),
),
),
Column
垂直排列,不换行(太多会向下溢出(需要放在 SingleChildScrollView 中才能滚动))
Column(
children: List.generate(
5,
(i) => Container(
height: 60,
color: Colors.green[100 * (i % 9)],
margin: EdgeInsets.all(4),
child: Center(child: Text('$i')),
),
),
),
Expanded
占满(Row、Column)的剩余空间
Wrap
自动换行,像标签云(自动换行,不会溢出,非常自然)
Wrap(
spacing: 8.0, // 水平间距
runSpacing: 8.0, // 垂直间距(换行后)
alignment: WrapAlignment.start,
children: List.generate(typeList.length, (i) {
final item = typeList[i];
return Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.orange[100 * (i % 9)],
borderRadius: BorderRadius.circular(20),
),
child: Text('标签 $i'),
);
}),
),
实际项目中使用建议
- 需要固定一行按钮或图标 → 用 Row
- 需要垂直堆叠内容(如个人信息、列表项) → 用 Column
- 子组件数量不确定,想自动换行(如标签、表情、芯片) → 用 Wrap
- 想让 Row/Column 也能滚动 → 包裹在 SingleChildScrollView 中
- 想让 Wrap 支持滚动 → 同样可以放进 SingleChildScrollView
SingleChildScrollView
SingleChildScrollView 是 Flutter 中最简单的滚动容器,它的作用是为单个子组件添加滚动能力。
核心特点:
- 只接受一个 child(子组件)。
- 当 child 的尺寸超过屏幕时,自动提供滚动(垂直或水平)。
- 不负责布局,只负责“让内容可滚动”。
- 性能开销极小,适合内容不太长或子组件简单的场景。
常用参数:
- scrollDirection: Axis.vertical(默认垂直滚动)或 Axis.horizontal(水平滚动)。
- physics:滚动物理效果(如 BouncingScrollPhysics iOS 回弹)。
- padding、clipBehavior 等。
注意:下面这行代码会强制 SingleChildScrollView 即使内容短也能响应滚动/下拉手势
physics: const AlwaysScrollableScrollPhysics(),
适用场景:
- 长页面整体滚动(表单、详情页)。
- 水平滚动图片轮播(配合 Row)。
- 标签云可滚动(配合 Wrap)。
- 内容高度不确定但不太多的情况。
注意:
- 如果数据量很大(数百上千项),不推荐用 SingleChildScrollView + Column(会一次性构建所有子组件,性能差)。
- 这种情况下应该用 ListView或与ListView类似的组件。
列表(ListView、GridView、StaggeredGridView)
这三个都是内置或常用的可滚动列表/网格组件,它们自带滚动能力(内部已包裹类似 SingleChildScrollView 的机制),并且支持懒加载(builder 模式),非常适合大数据量场景。
| Widget | 布局类型 | 子组件特点 | 懒加载支持 | 典型场景 | 需要额外包? |
|---|---|---|---|---|---|
| ListView | 垂直列表(可水平) | 单列,子组件高度可不同 | 是(.builder) | 聊天记录、新闻feed、设置页面、长列表 | 无(内置) |
| GridView | 规则网格 | 多列,所有子组件宽高统一 | 是(.builder) | 商品列表、相册、表情包、图标选择 | 无(内置) |
| StaggeredGridView | 不规则网格(瀑布流) | 多列,每项高度可不同 | 是 | Pinterest 图片墙、动态卡片、内容高度不一的列表 | 是(flutter_staggered_grid_view) |
共同优点(相比 SingleChildScrollView):
- 支持懒加载(.builder 构造函数):只构建可见项,性能极好,适合成百上千甚至上万数据。
- 自带滚动,无需额外包裹。
- 支持分隔线(ListView.separated)、缓存(cacheExtent)等优化。
ListView
适用于绝大多数垂直滚动列表
ListView.builder(
itemCount: 50, // 假设有50条数据
itemBuilder: (context, index) {
return Container();
},
),
ListView 水平布局
List<String> items = ['苹果', '香蕉', '橙子', '芒果', '菠萝', '草莓'];
SizedBox(
height: 140, // 必须给高度,否则 ListView 不知道怎么布局
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: items.length,
itemBuilder: (context, index) {
return Container(
width: 100,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(12),
),
child: Center(child: Text(items[index])),
);
},
),
)
为什么加 SizedBox(height: ...) ?
水平 ListView 需要明确的高度(否则会报错或高度塌陷为 0)
GridView
所有格子大小一致,整齐美观
GridView.builder(
padding: const EdgeInsets.all(10),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 每行显示3列
crossAxisSpacing: 10, // 列间距
mainAxisSpacing: 10, // 行间距
childAspectRatio: 1.0, // 子项宽高比(1.0 表示正方形)
),
itemCount: 50,
itemBuilder: (context, index) {
return Container();
},
),
注意:如果ListView、GridView组件是SingleChildScrollView组件的子组件那么需要加上下面两个属性,不然会报错:
- SingleChildScrollView和ListView/GridView都是滚动组件,嵌套时会导致"谁滚动谁"的问题。
- ListView/GridView默认会占用无限高度,导致布局溢出。
- 解决方案: 让ListView/GridView的高度自适应并禁止内部滚动
shrinkWrap: true, // 让高度自适应
physics: NeverScrollableScrollPhysics(), // 禁止内部滚动
清除ListView、GridView组件自带的边距:
padding: EdgeInsets.zero, // 清除 ListView 的默认内边距);
StaggeredGridView
瀑布流,高度由内容决定,排列紧凑不浪费空间,视觉效果最佳
注意需要安装插件:flutter_staggered_grid_view
具体使用请看官方文档
// 瀑布流布局
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
MasonryGridView.count(
shrinkWrap: true, // 让高度自适应
physics: NeverScrollableScrollPhysics(), // 禁止内部滚动
crossAxisCount: 2, // 固定 2 列
mainAxisSpacing: 12, // 垂直间距
crossAxisSpacing: 12, // 水平间距
itemCount: 5,
itemBuilder: (context, index) {
// 模拟不同高度(实际项目中高度由内容决定,比如图片+文字)
final height =
100 + (index % 5) * 40.0; // 示例:100~260px 随机高度
return Container(
height: height,
decoration: BoxDecoration(
color:
Colors.primaries[index %
Colors.primaries.length],
borderRadius: BorderRadius.circular(12),
),
alignment: Alignment.center,
child: Text(
'${index + 1}',
style: const TextStyle(
fontSize: 32,
color: Colors.white,
),
),
);
},
),
虚线/实线
实线
// 方式一
Divider(color: Color(0xff292D3B), thickness: 1),
// 方式二
Container(height: 1.h, width: 168.w, color: Color(0xff006CBA)),
虚线
SizedBox(
height: 1, // 厚度
child: LayoutBuilder(
builder:
(
BuildContext context,
BoxConstraints constraints,
) {
const double dashWidth =
4.0; // 每段短線的寬度(可調整,2~6 都行)
const double dashSpace = 4.0; // 短線之間的間距(可調整)
final int dashCount =
(constraints.maxWidth /
(dashWidth + dashSpace))
.floor() +
1;
return Flex(
direction: Axis.horizontal,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: List.generate(dashCount, (_) {
return SizedBox(
width: dashWidth,
height: 1,
child: DecoratedBox(
decoration: BoxDecoration(
color: Color(0xff292D3B),
),
),
);
}),
);
},
),
),
实现点击事件
flutter中的点击事件需要GestureDetector
GestureDetector(onTap: () {}, child: Container()),
裁剪
Flutter 中提供了多种裁剪(Clip)组件,它们的作用是强制限制子组件的绘制区域,防止内容溢出指定的形状,从而实现圆角、圆形、矩形或自定义形状的视觉效果。
裁剪组件本质上是在绘制阶段“剪掉”子组件超出部分的内容,非常适合实现圆角图片、圆形头像、波浪边框等 UI。
常用裁剪组件对比
| 组件名称 | 裁剪形状 | 主要用途 | 是否支持抗锯齿(推荐) | 性能/灵活性 |
|---|---|---|---|---|
| ClipRRect | 圆角矩形(Rounded Rect) | 最常用:圆角卡片、圆角图片、圆角容器 | 是(默认 antiAlias) | 高 |
| ClipRect | 直角矩形 | 简单矩形裁剪,避免内容溢出 | 可选 | 高 |
| ClipOval | 椭圆 / 正圆(宽高相等时为圆) | 圆形头像、圆形按钮、圆形图标 | 是 | 高 |
| ClipPath | 任意自定义路径 | 复杂形状(如波浪、心形、五角星、对话气泡箭头) | 可选 | 中(稍复杂) |
详细介绍每个组件
1. ClipRRect(最常用!)
-
作用:将子组件裁剪成圆角矩形。
-
关键参数:
- borderRadius:圆角半径(BorderRadius.circular(20) 等)。
- clipBehavior:裁剪模式,默认 Clip.antiAlias(抗锯齿,边缘更平滑)。
示例:圆角图片卡片
ClipRRect(
borderRadius: BorderRadius.circular(16), // 圆角16
child: Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover, // 关键:填满并被裁剪
),
),
2. ClipOval(专为圆形设计)
- 作用:裁剪成椭圆,宽高相等时就是正圆。
- 无需设置 radius,直接根据容器大小自动计算。
示例:圆形头像
ClipOval(
child: Image.network(
'https://example.com/avatar.jpg',
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
更推荐:对于纯圆形头像,Flutter 官方提供了 CircleAvatar(内置 ClipOval + 更多头像功能),优先用它。
3. ClipRect(简单矩形裁剪)
- 作用:裁剪成直角矩形,常用于防止子组件溢出父容器。
- 适合临时限制范围。
示例:只显示图片上半部分
ClipRect(
child: Image.network(
'https://example.com/tall-image.jpg',
width: 200,
height: 100, // 只显示前100高度
fit: BoxFit.cover,
),
),
4. ClipPath(最灵活,自定义形状)
- 作用:通过 CustomClipper 定义任意路径裁剪。
- 适合高级 UI:对话气泡小箭头、票券虚线边、不规则卡片等。
示例:三角形裁剪
ClipPath(
clipper: TriangleClipper(),
child: Container(
width: 200,
height: 200,
color: Colors.purple,
),
)
// 自定义 Clipper
class TriangleClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(size.width / 2, size.height);
path.lineTo(size.width, 0);
path.close();
return path;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}
使用建议总结
| 需求场景 | 推荐组件 | 原因 |
|---|---|---|
| 圆角图片、卡片 | ClipRRect | 最常用、最简单、支持抗锯齿 |
| 圆形头像 | ClipOval 或 CircleAvatar | ClipOval 更底层,CircleAvatar 更语义化 |
| 防止内容溢出矩形区域 | ClipRect | 轻量高效 |
| 复杂不规则形状(波浪、箭头) | ClipPath | 完全自定义 |
| Container 已有圆角但子元素不裁剪 | 加 ClipRRect 或 Container(clipBehavior: Clip.hardEdge) | 解决常见“圆角失效”问题 |
性能提示:裁剪操作在 GPU 上执行,性能很好,放心使用。但避免在高频动画中频繁重建 CustomClipper。
CircleAvatar 实现圆形头像
CircleAvatar(
radius: 50, // 半径(直径=100),控制大小
backgroundImage: NetworkImage('https://example.com/user-avatar.jpg'), // 网络图片
// backgroundImage: AssetImage('assets/avatar.png'), // 本地资产图片
// backgroundColor: Colors.grey, // 无图片时的背景色
// child: Text('P', style: TextStyle(fontSize: 40, color: Colors.white)), // 无图片时显示首字母
),
需求:头像外层加边框和阴影
CircleAvatar(
radius: 50,
backgroundColor: Colors.white, // 边框颜色
foregroundImage: NetworkImage('https://example.com/avatar.jpg'),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
),
),
输入框
- 记得在页面销毁时执行:phoneFocusNode.dispose();
- 关闭所有焦点:FocusScope.of(context).unfocus();
- 获取焦点:phoneFocusNode.requestFocus();
final TextEditingController phoneController = TextEditingController();
final FocusNode phoneFocusNode = FocusNode();
Future<void> handlePhone() async {}
TextField(
controller: phoneController,
focusNode: phoneFocusNode,
keyboardType: TextInputType.phone, // 输入的类型
cursorColor: Color(0xff63C1C4), // 输入框光标颜色
style: TextStyle(color: Colors.black, fontSize: 14.sp), // 文本样式
textInputAction: TextInputAction.go, // 设置键盘右下角按键的文本(或图标)
onSubmitted: (value) => handlePhone(), // 点击键盘右下角按键的回调
decoration: InputDecoration(
hintText: '请输入手机号',
hintStyle: TextStyle(
color: Color(0xff9a9a9a),
fontSize: 14.sp,
), // 占位符样式
// 未激活时边框样式
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xffffffff)),
borderRadius: BorderRadius.all(Radius.circular(15.r)),
),
// 激活时边框样式
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Color(0xff63C1C4)),
borderRadius: BorderRadius.all(Radius.circular(15.r)),
),
isDense: true, // 缩小输入框高度
// 输入框内边距
contentPadding: EdgeInsets.symmetric(
vertical: 12.h,
horizontal: 20.w,
),
// 前置图标
prefixIcon: Container(
padding: EdgeInsets.only(left: 20.w),
child: Image.asset(
'assets/images/login/sjh.png',
width: 22.w,
height: 22.w,
),
),
// 前置图标取消默认的最小约束
prefixIconConstraints: const BoxConstraints(
minWidth: 0,
minHeight: 0,
),
),
),
多行输入的文本域
多行输入的文本域用的也是输入框:TextField,只不过是加上一些属性:
| 属性 | 说明 | 常见值 |
|---|---|---|
maxLines | 最大行数 | null(无限)、5、8 等 |
minLines | 最小显示行数 | 1、3、4 |
maxLength | 最大字符数 | 100、500、2000 等 |
按钮
Flutter 中提供了丰富的按钮组件,主要基于 Material Design 风格(Material 3 已全面更新)。下面介绍最常用的几种按钮,以及它们的特点和适用场景。
| 按钮类型 | Widget 名 | 外观特点 | 推荐场景 | 旧版对应(已弃用) |
|---|---|---|---|---|
| FilledButton | FilledButton | 实心填充背景色,无阴影(默认主按钮) | 主要动作(Primary Action),如“提交”“确认” | RaisedButton |
| FilledButton.tonal | FilledButton.tonal | 次级填充(柔和色调) | 次要但重要的动作 | - |
| ElevatedButton | ElevatedButton | 实心背景 + 阴影(有立体感) | 需要突出但不是最主要的动作 | RaisedButton |
| OutlinedButton | OutlinedButton | 只有描边,无填充 | 中等重要动作,如“取消”“下一步” | OutlineButton |
| TextButton | TextButton | 纯文本,无背景无边框 | 次要动作、链接风格,如“查看更多” | FlatButton |
| IconButton | IconButton | 只显示图标,水波纹点击效果 | 工具栏、列表项操作(如删除、喜欢) | - |
特殊按钮
-
FloatingActionButton(FAB) 圆形、浮动在内容之上,通常用于最重要的动作(如“添加”)。
FloatingActionButton( onPressed: () {}, child: Icon(Icons.add), )
代码示例(常用按钮对比)
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FilledButton(onPressed: () {}, child: Text('主要按钮')),
FilledButton.tonal(onPressed: () {}, child: Text('柔和填充按钮')),
ElevatedButton(onPressed: () {}, child: Text('凸起按钮')),
OutlinedButton(onPressed: () {}, child: Text('描边按钮')),
TextButton(onPressed: () {}, child: Text('文本按钮')),
IconButton(onPressed: () {}, icon: Icon(Icons.favorite)),
],
)
自定义样式
所有按钮都支持 style 参数,可以统一主题或单独自定义:
FilledButton(
onPressed: () {},
style: FilledButton.styleFrom(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
child: Text('自定义按钮'),
)
选择建议
- 最重要的动作 → FilledButton(默认主题色最显眼)
- 次要动作 → FilledButton.tonal 或 ElevatedButton
- 取消/中性动作 → OutlinedButton
- 内联文字操作 → TextButton
- 图标操作 → IconButton
- 页面核心操作 → FloatingActionButton
绝对定位
在 Flutter 中,没有直接对应的 CSS position: absolute,但可以通过几种方式实现类似绝对定位的效果(即让子组件相对于父容器某个角落、边缘或任意位置定位,而不参与正常流式布局)。
下面按推荐顺序介绍最常用的几种方法:
1. 最推荐:Stack + Positioned(最接近 CSS absolute)
Stack 是 Flutter 的“层叠布局”,允许子组件叠加。 Positioned 用于在 Stack 中对子组件进行绝对定位。
Stack(
children: [
// 背景或基础内容
Container(color: Colors.blue[100], width: 300, height: 200),
// 绝对定位的子组件
Positioned(
left: 20, // 距离左边 20
top: 30, // 距离顶部 30
child: Container(width: 80, height: 80, color: Colors.red),
),
Positioned(
right: 10, // 距离右边 10
bottom: 15, // 距离底部 15
child: Icon(Icons.favorite, color: Colors.pink, size: 40),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(height: 50, color: Colors.black54, child: Center(child: Text('标题栏', style: TextStyle(color: Colors.white)))),
),
],
)
Positioned 参数(类似 CSS):
- left、top、right、bottom:分别控制距离父容器四边的距离(可组合使用)。
- width、height:固定宽高(可选)。
- 如果不指定 width/height,子组件会根据自身内容大小决定。
适用场景:头像上的小红点、浮动按钮、卡片上的标签、复杂叠加布局等。
2. Stack + Align(快速居中或角落定位)
适合快速把子组件放到 Stack 的九宫格位置(类似 CSS 的 top-left、center 等)。
Stack(
children: [
Container(color: Colors.grey[300], width: 200, height: 200),
Align(
alignment: Alignment.topLeft, // 左上角
child: Icon(Icons.star, color: Colors.yellow),
),
Align(
alignment: Alignment.center, // 正中心
child: Text('中心文字'),
),
Align(
alignment: Alignment.bottomRight, // 右下角
child: FloatingActionButton.mini(onPressed: () {}, child: Icon(Icons.add)),
),
],
)
常用 Alignment 值:
- Alignment.topLeft、topCenter、topRight
- centerLeft、center、centerRight
- bottomLeft、bottomCenter、bottomRight
- 自定义:Alignment(0.5, -0.8)(x/y 范围 -1~1)
3. Stack + Transform(偏移定位)
适合从某个基准点平移(类似 CSS transform: translate)。
Stack(
children: [
Container(color: Colors.green[200]),
Transform.translate(
offset: Offset(50, 100), // x 偏移 50,y 偏移 100
child: Container(width: 60, height: 60, color: Colors.orange),
),
],
)
子元素溢出Stack被裁剪问题
Stack组件默认是会裁剪边界之外的内容,解决方案:
声明属性:clipBehavior: Clip.none(允许子组件溢出 Stack 边界)
总结推荐
| CSS 效果 | Flutter 实现方式 |
|---|---|
| position: absolute | Stack + Positioned(最常用) |
| top/left/right/bottom | Positioned 的对应参数 |
| 居中 | Stack + Align(Alignment.center) |
| 角落定位 | Align 的 topLeft / bottomRight 等 |
| 偏移 | Transform.translate |
进行 3D/2D 几何变换(Transform)
Transform 是 Flutter 中一个强大的布局 Widget,它的作用是对子组件应用矩阵变换(Matrix4) ,从而实现**平移(translate)、旋转(rotate)、缩放(scale)、倾斜(skew)**等视觉效果。
简单来说:Transform = 对 child 进行 3D/2D 几何变换,常用于动画、倾斜卡片、偏移定位、3D 翻转等酷炫 UI。
核心特点
- 不改变子组件在布局流中的位置(布局仍按原位置计算)。
- 只影响绘制阶段(视觉上移动/旋转,但点击区域通常不变,除非用 Transform.hitTest 调整)。
- 支持 4x4 矩阵(Matrix4),几乎所有 2D/3D 变换都能实现。
常用构造函数(推荐使用这些静态方法,更简单)
| 方法 | 作用 | 参数示例 |
|---|---|---|
Transform.translate | 平移(移动位置) | offset: Offset(dx, dy) |
Transform.rotate | 旋转 | angle: 角度(弧度),origin: 旋转中心 |
Transform.scale | 缩放 | scaleX、scaleY(或统一 scale) |
Transform.skew | 倾斜(错切) | skewX、skewY(弧度) |
代码示例
1. 平移(类似负 margin 或偏移)
Transform.translate(
offset: Offset(20, -30), // x 右移20,y 上移30(负值向上)
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text('向上偏移')),
),
)
2. 旋转(常见卡片倾斜效果)
Transform.rotate(
angle: 0.2, // 约 11.5 度(π ≈ 3.14,0.2 ≈ 11.5°)
origin: Offset(0, 50), // 以左上角向下50px处为旋转中心
child: Container(
width: 150,
height: 100,
color: Colors.green,
child: Center(child: Text('旋转卡片')),
),
)
3. 缩放(放大/缩小)
Transform.scale(
scale: 1.5, // 放大 1.5 倍
child: Icon(Icons.favorite, size: 50, color: Colors.red),
)
4. 组合变换(高级:矩阵方式)
Transform(
transform: Matrix4.identity()
..rotateZ(0.1) // 先旋转
..scale(1.2) // 再放大
..translate(30, 0), // 最后平移
child: YourWidget(),
)
常见应用场景
- 负 margin 效果:用 translate 向上/左偏移(你之前问的重叠布局常用)。
- 倾斜卡片:rotate 小角度 + 阴影 → 立体感。
- 3D 翻转动画:配合 AnimationController 用 Matrix4.rotationY。
- 自定义动画:在 AnimatedBuilder 中动态改变 Transform 参数。
注意事项
- 点击区域不随变换:旋转后点击区域仍是原矩形(可用 Matrix4 的 hitTest 调整,但复杂)。
- 性能:轻量,但嵌套太多或复杂矩阵可能影响 GPU。
- alignment 参数(在构造函数中):控制变换的原点(类似 CSS transform-origin),默认中心。
与其他组件对比
| 需求 | 推荐方式 |
|---|---|
| 简单偏移 | Transform.translate |
| 动画变换 | AnimatedContainer 或 Transform + Animation |
| 仅布局偏移(不视觉) | 用 Padding/Margin/Positioned |
总结: Transform 是 Flutter 中实现“视觉变换”的核心工具,配合 Stack、Animation,能做出各种高级 UI 效果。 日常最常用的是 Transform.translate(负值偏移)和 Transform.rotate(倾斜)。
动画(Animation)
Flutter 中的动画(Animation) 是让 UI 元素随时间平滑变化的核心机制,比如淡入淡出、缩放、平移、旋转、颜色渐变等,能极大提升 App 的交互感和精致度。
Flutter 的动画系统设计得非常强大、灵活且性能优秀(全部在 GPU 上运行),分为隐式动画和显式动画两大类。
1. 隐式动画(Implicit Animation)—— 最简单、最常用
特点:只需指定“目标状态”,Flutter 自动处理从当前到目标的过渡动画。 适合:大多数日常动画(如按钮点击变大、容器颜色渐变等)。
常见隐式动画 Widget(直接在原 Widget 前加 “Animated”):
| Widget | 作用 | 示例 |
|---|---|---|
AnimatedContainer | 宽高、颜色、圆角、边框等变化动画 | 背景色渐变、卡片展开 |
AnimatedOpacity | 透明度渐变(淡入淡出) | 加载提示淡出、图片渐现 |
AnimatedAlign | 对齐方式变化动画 | 子组件从左移到右 |
AnimatedPadding | 内边距变化动画 | 按钮点击时扩大内边距 |
AnimatedPositioned | 在 Stack 中位置变化动画 | 浮动按钮移动 |
AnimatedScale | 缩放动画(Flutter 3.7+) | 图标点击放大 |
AnimatedRotation | 旋转动画 | 箭头展开/收起 |
示例:点击按钮变大变色
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: Duration(milliseconds: 300), // 动画时长(必须)
curve: Curves.easeInOut, // 动画曲线(可选)
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
color: _expanded ? Colors.red : Colors.blue,
child: Center(child: Text('点击我')),
),
);
}
}
2. 显式动画(Explicit Animation)—— 更强大、更可控
特点:需要手动创建 AnimationController 和 Animation,适合复杂、同步、多阶段或需要精确控制的动画。
核心组件:
- AnimationController:动画的“控制器”,控制时长、正逆向播放、重复等。
- Animation:具体的值变化(如 double、Color)。
- Tween:定义起始值和结束值。
- CurvedAnimation:添加缓动曲线。
常用显式动画 Widget:
- TweenAnimationBuilder:最简单显式动画(推荐入门)。
- AnimatedBuilder:性能更好,适合复杂动画。
示例:用 TweenAnimationBuilder 实现旋转动画
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(seconds: 2),
builder: (context, value, child) {
return Transform.rotate(
angle: value * 2 * pi, // 旋转一圈
child: Icon(Icons.refresh, size: 50),
);
},
)
示例:经典 AnimationController(加载旋转圈)
class LoadingWidget extends StatefulWidget {
@override
State<LoadingWidget> createState() => _LoadingWidgetState();
}
class _LoadingWidgetState extends State<LoadingWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, // 必须(提供 ticker)
duration: Duration(seconds: 1),
)..repeat(); // 无限重复
}
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _controller, // 直接用 controller
child: Icon(Icons.sync, size: 40),
);
}
@override
void dispose() {
_controller.dispose(); // 必须释放
super.dispose();
}
}
总结建议
| 需求场景 | 推荐方式 |
|---|---|
| 简单状态变化(颜色、大小) | 隐式动画(AnimatedContainer 等) |
| 淡入淡出、透明度 | AnimatedOpacity |
| 需要重复、循环、精确控制 | 显式动画 + AnimationController |
| 快速自定义值变化 | TweenAnimationBuilder |
| 页面切换共享元素 | Hero 动画 |
使用技巧
深浅拷贝
浅拷贝
用于大多数业务场景 适用场景:
- data 和 arr 里面的值主要是基本类型(String、int、double、bool、null)
- 或者你不关心嵌套对象/数组被修改(浅拷贝只复制第一层)
void handlePlaySound(Map<String, dynamic> data, List<dynamic> arr) {
// 方式1: 创建浅拷贝(最常用、最快)
final dataCopy = Map<String, dynamic>.from(data);
final arrCopy = List<dynamic>.from(arr);
// 方式2:也是浅拷贝,但是没指定数据类型
final dataCopy = {...data};
final arrCopy = [...arr];
// 现在你可以随便修改 dataCopy 和 arrCopy
dataCopy['volume'] = 0.8;
dataCopy.remove('tempKey');
arrCopy.add('new_item');
arrCopy.removeAt(0);
arrCopy[1] = 'modified';
// 后续逻辑使用 dataCopy 和 arrCopy
// 原 data 和 arr 完全不受影响
}
深拷贝
当 data 或 arr 里面有嵌套的 Map 或 List,并且你希望修改嵌套层也不影响原数据时,需要深拷贝。
注意: json 深拷贝的限制:
- 只支持可序列化的类型(DateTime、自定义类不行)
- 会丢失一些特殊类型(Function、Symbol 等)
- 性能比浅拷贝低一些
import 'dart:convert'; // 用于 json 方法实现深拷贝
void handlePlaySound(Map<String, dynamic> data, List<dynamic> arr) {
final dataCopy = jsonDecode(jsonEncode(data)) as Map<String, dynamic>;
final arrCopy = jsonDecode(jsonEncode(arr)) as List<dynamic>;
// 现在修改 dataCopy 和 arrCopy 完全安全
dataCopy['nested']?['child'] = 'new value';
arrCopy[2]?['sub'] = 999;
}
在数组中根据id查找元素(firstWhere)
Map<String, dynamic> d2 = {'id': 1};
List<Map<String, dynamic>> arr2 = [
{'id': 1},
{'id': 2},
{'id': 3},
];
var foundItem = arr2.firstWhere((item) => item['id'] == d2['id']);
在数组中根据id查找元素的索引(indexWhere)
Map<String, dynamic> d2 = {'id': 1};
List<Map<String, dynamic>> arr2 = [
{'id': 1},
{'id': 2},
{'id': 3},
];
// 找到第一个 id 相等的元素的索引
int index = arr2.indexWhere((item) => item['id'] == d2['id']);
想访问数组中每一个元素和索引
List arr2 = [
{'id': 1},
{'id': 2},
{'id': 3},
];
// 1.使用forin + .indexed(支持await)
for (final (index, item) in arr2.indexed) {
print('索引: $index, 元素: $item');
// 或使用 item['id']
print('id 值: ${item['id']}');
}
// 2.使用.asMap().entries(支持await)
for (final entry in arr2.asMap().entries) {
final index = entry.key;
final item = entry.value;
print('索引: $index, 元素: $item');
print('id: ${item['id']}');
}
// 3.使用forEach(不支持await)
arr2.asMap().forEach((index, item) {
print('索引: $index → 元素: $item → id: ${item['id']}');
});
// 4.使用传统 for 循环(支持await)
for (int i = 0; i < arr2.length; i++) {
final item = arr2[i];
print('索引: $i, 元素: $item, id: ${item['id']}');
// 如果需要修改元素
arr2[i]['newKey'] = 'value';
}