本章摘要
本章讲解 Container、DecoratedBox、ColoredBox、Card、Material 及裁剪组件的底层关系,重点说明尺寸与约束、内外边距、背景与前景装饰、圆角和裁剪的区别,以及水波纹被遮挡、图片未裁圆角和过度裁剪等常见问题。
一、什么是单子组件容器
单子组件容器通常只有一个:
Widget? child
例如:
Padding(
child: child,
);
Center(
child: child,
);
Container(
child: child,
);
它们一般负责以下某一项或多项工作:
- 设置尺寸
- 添加间距
- 对齐 child
- 绘制背景
- 绘制边框和阴影
- 裁剪 child
- 提供 Material 表面
- 对 child 进行变换
不同 Widget 的职责不同。
例如,只需要背景色时,可以使用:
ColoredBox(
color: Colors.blue,
child: child,
);
需要圆角、边框和渐变时,可以使用:
DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: const LinearGradient(
colors: [
Colors.blue,
Colors.purple,
],
),
),
child: child,
);
需要同时处理尺寸、间距、对齐和装饰时,使用 Container 更方便。
二、Container 的本质
1. Container 不是一个简单的基础渲染组件
Container 是一个便利型 Widget,它把常见的:
- Padding
- Align
- ColoredBox
- DecoratedBox
- ConstrainedBox
- Transform
- Margin
等能力组合在一起。官方将它定义为组合常见绘制、定位和尺寸功能的便利组件。
例如:
Container(
width: 200,
height: 100,
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(8),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: const Text('Hello'),
);
可以概念化地理解为:
外层 Margin
↓
尺寸与约束
↓
背景 Decoration
↓
Padding
↓
Align
↓
Text
这不是 Container 源码的逐行等价代码,但可以帮助理解它同时承担的职责。
三、Container 的布局顺序
Container 的布局过程可以简化为:
1. 处理 child 和 alignment
2. 加入 padding
3. 加入 decoration 边框需要的内部空间
4. 应用 width、height 和 constraints
5. 在最外层加入 margin
Container 会先使用 Padding 包围 child,并把 Decoration 边框需要的空间计算进去;然后应用 width、height 和额外约束;最后在外部加入 margin。
四、Container 的绘制顺序
Container 的绘制顺序大致是:
1. 应用 transform
2. 绘制 decoration
3. 绘制 child
4. 绘制 foregroundDecoration
因此:
decoration
绘制在 child 后面。
而:
foregroundDecoration
绘制在 child 前面。
五、width 和 height 如何转成约束
假设:
Container(
width: 200,
height: 100,
);
Container 构造函数会把它们转换为类似:
BoxConstraints.tightFor(
width: 200,
height: 100,
);
如果同时提供了额外约束,则会通过 tighten 把指定宽高加入现有约束。
可以简化理解为:
width: 200
→ minWidth = 200
→ maxWidth = 200
height: 100
→ minHeight = 100
→ maxHeight = 100
但仍然需要满足父组件传下来的约束。
父组件约束仍然优先
SizedBox(
width: 150,
child: Container(
width: 300,
height: 100,
color: Colors.blue,
),
);
外层已经要求宽度为 150。
因此内层 Container 不能真的显示为 300。
最终布局仍遵循:
父组件传约束
子组件在约束内选尺寸
六、width 和 height 是否包含 Padding
以下代码:
Container(
width: 200,
height: 100,
padding: const EdgeInsets.all(20),
child: child,
);
Container 整体尺寸是:
200 × 100
而不是:
240 × 140
width 和 height 包含 Padding,因此 child 实际可用空间大致为:
宽度:200 - 20 - 20 = 160
高度:100 - 20 - 20 = 60
前提是没有其他边框空间,并且父约束允许该尺寸。
七、Padding 和 Margin 的区别
Padding
Padding 是 decoration 与 child 之间的内部空间:
Container(
padding: const EdgeInsets.all(16),
color: Colors.blue,
child: const Text('Hello'),
);
结构:
蓝色背景
↓
16 像素内部空白
↓
Text
Margin
Margin 是 Container 与外部组件之间的空间:
Container(
margin: const EdgeInsets.all(16),
color: Colors.blue,
child: const Text('Hello'),
);
结构:
外部组件
↓
16 像素空白
↓
蓝色 Container
Container 的 decoration 不会绘制到 margin 区域。Margin 是围绕 decoration 和 child 的外部空白。
同时使用
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(12),
color: Colors.blue,
child: const Text(
'Hello',
style: TextStyle(color: Colors.white),
),
);
可以理解为:
Margin 16
↓
蓝色背景
↓
Padding 12
↓
Text
八、Container 没有 child 时有多大
下面的 Container:
Container(
color: Colors.blue,
);
如果父组件提供有限空间,它通常会尽可能扩展。
例如放在 Scaffold 的 body 中:
Scaffold(
body: Container(
color: Colors.blue,
),
);
可能占满整个 body。
但如果处在无界约束中,它会尝试尽可能缩小。Container 有 child 时,默认会优先根据 child 的大小确定尺寸;width、height 和 constraints 可以改变这一行为。
九、color 和 decoration 为什么不能同时使用
错误:
Container(
color: Colors.blue,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
),
);
Flutter 会抛出断言错误:
Cannot provide both a color and a decoration.
原因是:
color: Colors.blue
本质上是简单写法:
decoration: const BoxDecoration(
color: Colors.blue,
)
如果两者同时提供,就会出现两个背景定义。Flutter 要求需要其他装饰时,把颜色一起写入 BoxDecoration。
正确:
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
);
十、什么时候直接使用 color
只需要纯色背景:
Container(
color: Colors.blue,
child: child,
);
或者更明确地使用:
ColoredBox(
color: Colors.blue,
child: child,
);
Flutter 会针对简单颜色场景使用 ColoredBox 进行处理。
需要以下效果时再使用 Decoration:
- 圆角
- 边框
- 阴影
- 渐变
- 背景图片
- 圆形背景
十一、BoxDecoration
BoxDecoration 用于描述一个盒子的视觉效果。
常用参数:
BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.grey,
width: 1,
),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 12,
offset: Offset(0, 4),
),
],
);
它支持:
- color
- gradient
- image
- border
- borderRadius
- boxShadow
- shape
- backgroundBlendMode
未设置某项时,就不会绘制对应效果。没有 borderRadius 时,背景绘制还可以使用更简单的绘制命令。
十二、背景颜色
Container(
decoration: const BoxDecoration(
color: Colors.blue,
),
);
颜色会填充到 BoxDecoration 定义的形状中。
例如:
const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
);
会绘制圆形蓝色背景。
十三、渐变背景
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue,
Colors.purple,
],
),
),
);
Flutter 常见渐变包括:
LinearGradient
RadialGradient
SweepGradient
当 gradient 不为空时,BoxDecoration 的 color 不再产生效果;渐变会绘制在 DecorationImage 下面。
因此不要同时依赖:
color: Colors.blue
和:
gradient: ...
来显示两个独立背景。
十四、边框
Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.grey,
width: 1,
),
borderRadius: BorderRadius.circular(12),
),
);
边框会绘制在背景颜色、渐变或背景图片上方,并遵循 BoxDecoration 的形状和圆角。
单边边框:
const BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey,
width: 1,
),
),
);
适合:
- 分割线
- 输入框底边
- 列表项底边
- 顶部提示条
十五、阴影
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Color(0x22000000),
blurRadius: 16,
spreadRadius: 0,
offset: Offset(0, 6),
),
],
),
);
参数含义:
color
阴影颜色
offset
阴影偏移
blurRadius
模糊程度
spreadRadius
阴影向外扩展的范围
BoxShadow 可以跟随矩形、圆角矩形或圆形等非普通矩形形状。
十六、DecorationImage
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
image: const DecorationImage(
image: NetworkImage(
'https://example.com/photo.jpg',
),
fit: BoxFit.cover,
),
),
);
DecorationImage 会绘制在背景颜色或渐变上方,并支持:
- fit
- alignment
- opacity
- colorFilter
- repeat
- onError
背景图片会根据 BoxDecoration 的圆形或 borderRadius 进行对应形状绘制。
但这只影响 Decoration 自己绘制的背景图片,不代表普通 child 会自动被裁剪。
十七、decoration 和 foregroundDecoration
decoration
绘制在 child 后面:
Container(
decoration: const BoxDecoration(
color: Colors.blue,
),
child: const Text('Hello'),
);
结构:
蓝色背景
Text
foregroundDecoration
绘制在 child 前面:
Container(
foregroundDecoration: const BoxDecoration(
color: Color(0x66000000),
),
child: Image.network(photoUrl),
);
结构:
Image
半透明黑色前景层
foregroundDecoration 会覆盖 child。
使用场景
图片暗色遮罩
Container(
foregroundDecoration: const BoxDecoration(
color: Color(0x55000000),
),
child: Image.network(
photoUrl,
fit: BoxFit.cover,
),
);
禁用状态
Container(
foregroundDecoration: isDisabled
? const BoxDecoration(
color: Color(0x88FFFFFF),
)
: null,
child: child,
);
注意:前景装饰只是视觉覆盖,不会自动禁止点击。
需要禁止点击时,还应使用:
IgnorePointer(
ignoring: isDisabled,
child: child,
);
十八、borderRadius 不等于裁剪
这是非常常见的错误。
错误示例
Container(
width: 160,
height: 120,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
child: Image.network(
photoUrl,
fit: BoxFit.cover,
),
);
问题表现
Container 的背景是圆角,但图片仍然从四个直角位置显示。
原因
BoxDecoration.borderRadius 只描述 Decoration 如何绘制,并不会默认裁剪普通 child。官方明确说明,shape 和 borderRadius 不会自动裁剪 Container 的子组件。
正确写法一:ClipRRect
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
photoUrl,
width: 160,
height: 120,
fit: BoxFit.cover,
),
);
正确写法二:Container.clipBehavior
当前 Container 在存在 Decoration 时,也可以指定裁剪:
Container(
width: 160,
height: 120,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
child: Image.network(
photoUrl,
fit: BoxFit.cover,
),
);
Container 的 clipBehavior 只有在 decoration 不为空时可用,默认是 Clip.none。裁剪路径由 Decoration 提供。
十九、DecoratedBox
1. 作用
DecoratedBox 专门负责绘制 Decoration。
DecoratedBox(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: child,
);
它可以把 Decoration 绘制在:
DecorationPosition.background
或者:
DecorationPosition.foreground
默认绘制在 child 后面。
2. 与 Container 的区别
DecoratedBox
只重点负责:
绘制 Decoration
Container
可以同时负责:
尺寸
约束
Margin
Padding
Alignment
Decoration
Foreground Decoration
Transform
裁剪
如果只需要装饰:
DecoratedBox(
decoration: decoration,
child: child,
);
语义比 Container 更明确。
3. DecoratedBox 不自动裁剪
DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
child: Image.network(photoUrl),
);
图片仍然不会自动被裁圆角。
DecoratedBox 只绘制 Decoration,child 不会自动被剪裁。
二十、ColoredBox
ColoredBox 只负责:
用指定颜色填充自己的区域
再在颜色上绘制 child
const ColoredBox(
color: Colors.blue,
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'Hello',
style: TextStyle(color: Colors.white),
),
),
);
适合:
- 纯色背景
- 全屏背景
- 列表项状态背景
- 调试布局范围
ColoredBox 和 Container
只设置背景:
ColoredBox(
color: Colors.blue,
child: child,
);
比:
Container(
color: Colors.blue,
child: child,
);
职责更单一。
但如果同时需要:
- Padding
- Margin
- 宽高
- Alignment
使用 Container 会更方便。
二十一、Card
1. Card 的作用
Card 用于展示一组相关内容,通常具有:
- 背景表面
- 圆角形状
- 阴影或边框
- Margin
- Material 语义
Card 常与 ListTile、图片、按钮等组合使用。
Card(
child: ListTile(
leading: const CircleAvatar(),
title: const Text('Alice, 32'),
subtitle: const Text('Los Angeles'),
onTap: () {},
),
);
2. Material 3 中的 Card 类型
启用 Material 3 时,Flutter 提供:
Card()
Card.filled()
Card.outlined()
分别对应:
- Elevated Card
- Filled Card
- Outlined Card
它们共享 CardThemeData。
3. Elevated Card
Card(
elevation: 4,
child: child,
);
适合需要阴影层级的卡片。
4. Filled Card
Card.filled(
child: child,
);
主要通过填充颜色与页面背景区分。
5. Outlined Card
Card.outlined(
child: child,
);
主要通过边框表达区域。
6. Card 不一定默认裁剪 child
Card 的 clipBehavior 默认会读取主题配置;如果主题也没有设置,则为 Clip.none。
如果 Card 中的图片需要遵循 Card 圆角:
Card(
clipBehavior: Clip.antiAlias,
child: Image.network(
photoUrl,
fit: BoxFit.cover,
),
);
二十二、Material Widget
Material 不只是一个背景容器。
它主要提供:
- Material 形状和表面
- Elevation 和阴影
- Ink 水波纹的绘制区域
- 可选的形状裁剪
Material 的 elevation 会把子树放到对应的视觉高度并绘制阴影;InkWell 和 InkResponse 的水波纹也绘制在最近的 Material 上。
基本示例
Material(
color: Colors.white,
elevation: 4,
borderRadius: BorderRadius.circular(16),
child: child,
);
二十三、为什么 InkWell 需要 Material
InkWell(
onTap: () {},
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Open Profile'),
),
);
InkWell 的水波纹不是直接绘制在 InkWell 自己上。
它会把水波纹绘制到最近的 Material 表面。
因此没有 Material 祖先时,可能:
- 看不到标准水波纹
- 出现运行时提示
- 点击反馈不符合预期
正确:
Material(
color: Colors.white,
child: InkWell(
onTap: () {},
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Open Profile'),
),
),
);
二十四、为什么水波纹会被 Container 遮挡
错误示例
Material(
child: InkWell(
onTap: () {},
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.all(16),
child: const Text('Open Profile'),
),
),
);
问题表现
按钮可以点击,但水波纹可能看不到。
原因
水波纹绘制在 Material 上,而中间的 Container/DecoratedBox 又在 Material 上方绘制了不透明背景,把水波纹遮住了。官方建议这种情况把 Decoration 绘制到 Material 的 Ink 层上。
正确写法一:使用 Ink
Material(
color: Colors.transparent,
child: Ink(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {},
child: const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Open Profile',
style: TextStyle(color: Colors.white),
),
),
),
),
);
正确写法二:让 Material 自己绘制背景
Material(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () {},
child: const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Open Profile',
style: TextStyle(color: Colors.white),
),
),
),
);
二十五、ClipRect
ClipRect 使用普通矩形裁剪 child:
ClipRect(
child: child,
);
它会阻止 child 绘制到自己的矩形边界外。默认 clipBehavior 是:
Clip.hardEdge
显示图片上半部分
ClipRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: 0.5,
child: Image.network(photoUrl),
),
);
如果没有 ClipRect,Align 缩小自身高度后,图片仍可能绘制到布局区域外。
适合场景
- 裁剪超出矩形范围的绘制
- 自定义 Paint
- Align 显示图片的一部分
- OverflowBox
- 动画展开收起区域
二十六、ClipRRect
ClipRRect 使用圆角矩形裁剪 child:
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
photoUrl,
fit: BoxFit.cover,
),
);
它默认使用自身边界作为裁剪区域,默认裁剪模式为:
Clip.antiAlias
ClipRRect 尺寸不匹配问题
错误:
SizedBox(
width: 200,
height: 200,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Align(
child: SizedBox(
width: 100,
height: 100,
child: Image.network(photoUrl),
),
),
),
);
如果 ClipRRect 的边界比实际图片大很多,圆角位置可能与图片边缘不一致。
官方建议保证 ClipRRect 和 child 使用相同边界。
正确:
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SizedBox(
width: 100,
height: 100,
child: Image.network(
photoUrl,
fit: BoxFit.cover,
),
),
);
二十七、ClipOval
ClipOval 把 child 裁剪成椭圆:
ClipOval(
child: Image.network(
photoUrl,
width: 100,
height: 100,
fit: BoxFit.cover,
),
);
如果布局区域是正方形,结果是圆形。
如果区域是长方形:
width: 160
height: 100
结果是椭圆。ClipOval 默认使用 Clip.antiAlias。
圆形头像
ClipOval(
child: Image.network(
photoUrl,
width: 88,
height: 88,
fit: BoxFit.cover,
),
);
也可以使用:
CircleAvatar(
radius: 44,
backgroundImage: NetworkImage(photoUrl),
);
如果只是标准头像,CircleAvatar 通常更方便。
二十八、Clip 类型
Flutter 提供四种主要裁剪模式:
Clip.none
clipBehavior: Clip.none
不裁剪。
这是大多数组件的默认行为,可以避免不必要的裁剪成本。
Clip.hardEdge
clipBehavior: Clip.hardEdge
特点:
- 裁剪速度较快
- 不抗锯齿
- 曲线边缘可能不够平滑
适合:
- 轴对齐矩形
- 不需要平滑曲线的区域
Clip.antiAlias
clipBehavior: Clip.antiAlias
特点:
- 对边缘进行抗锯齿
- 圆角和斜线更平滑
- 比 hardEdge 稍慢
适合:
- 圆形头像
- 圆角图片
- 非矩形边缘
Clip.antiAliasWithSaveLayer
clipBehavior: Clip.antiAliasWithSaveLayer
它会额外创建离屏缓冲区,性能成本明显更高,应很少使用。大部分场景应优先选择 Clip.antiAlias。
二十九、裁剪的性能问题
裁剪通常比完全不裁剪更贵,但普通 ClipRect 或 ClipRRect 不一定会自动触发昂贵的 saveLayer。
真正需要特别避免的是:
Clip.antiAliasWithSaveLayer
Flutter 性能指南建议谨慎使用裁剪,并尽可能使用组件本身提供的圆角能力,而不是无必要地增加裁剪层。
不需要裁剪的场景
下面只是绘制圆角纯色背景:
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
),
);
没有 child 超出圆角边界,就不需要额外添加:
ClipRRect(...)
需要裁剪的场景
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
child: Image.network(photoUrl),
);
图片会覆盖圆角区域,此时才需要裁剪。
三十、Card 与 Container 如何选择
使用 Container
适合:
- 自定义布局
- 自定义背景
- 自定义边框
- 自定义阴影
- 与 Material Design 无关的普通盒子
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: child,
);
使用 Card
适合:
- 一组相关内容
- Material 风格卡片
- 需要 elevation
- 需要 CardTheme
- 需要 Card 的语义
Card(
child: child,
);
使用 Material
适合:
- 自定义 Material 表面
- 需要 InkWell 水波纹
- 需要形状和 elevation
- 不想使用 Card 的默认 Margin 和语义
Material(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
elevation: 2,
child: InkWell(
onTap: () {},
child: child,
),
);
三十一、常见错误与正确写法
错误一:color 和 decoration 同时使用
错误
Container(
color: Colors.blue,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
),
);
正确
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
);
错误二:圆角背景没有裁剪图片
错误
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
child: Image.network(photoUrl),
);
正确
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
photoUrl,
fit: BoxFit.cover,
),
);
错误三:水波纹被 Container 遮住
错误
Material(
child: InkWell(
onTap: () {},
child: Container(
color: Colors.blue,
child: child,
),
),
);
正确
Material(
color: Colors.blue,
child: InkWell(
onTap: () {},
child: child,
),
);
或者使用 Ink。
错误四:只设置背景却使用复杂 Container
不推荐
Container(
decoration: const BoxDecoration(
color: Colors.blue,
),
child: child,
);
更明确
ColoredBox(
color: Colors.blue,
child: child,
);
错误五:用 foregroundDecoration 禁止点击
错误理解
Container(
foregroundDecoration: disabled
? const BoxDecoration(
color: Colors.white54,
)
: null,
child: button,
);
这只是视觉变灰,按钮仍然可能可以点击。
正确
IgnorePointer(
ignoring: disabled,
child: Container(
foregroundDecoration: disabled
? const BoxDecoration(
color: Colors.white54,
)
: null,
child: button,
),
);
错误六:所有圆角都使用 ClipRRect
问题
即使 child 完全不会超出边界,也添加裁剪:
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Container(
color: Colors.blue,
),
);
更简单
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16),
),
);
只需要绘制圆角背景,不需要裁剪。
错误七:使用 antiAliasWithSaveLayer
不推荐
ClipRRect(
clipBehavior: Clip.antiAliasWithSaveLayer,
borderRadius: BorderRadius.circular(16),
child: image,
);
通常使用
ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(16),
child: image,
);
三十二、完整可运行示例
import 'package:flutter/material.dart';
void main() {
runApp(const ContainerDemoApp());
}
class ContainerDemoApp extends StatelessWidget {
const ContainerDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
),
useMaterial3: true,
),
home: const ContainerDemoPage(),
);
}
}
class ContainerDemoPage extends StatelessWidget {
const ContainerDemoPage({super.key});
static const photoUrl =
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Single-child Containers'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const SectionTitle('1. Container'),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
border: Border.all(
color: Colors.blue.shade200,
),
borderRadius: BorderRadius.circular(16),
),
child: const Text(
'Container can combine size, padding, alignment and decoration.',
),
),
const SizedBox(height: 24),
const SectionTitle('2. DecoratedBox'),
const SizedBox(height: 12),
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.deepPurple,
Colors.blue,
],
),
borderRadius: BorderRadius.all(
Radius.circular(16),
),
),
child: Padding(
padding: EdgeInsets.all(20),
child: Text(
'Gradient background',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 24),
const SectionTitle('3. ClipRRect'),
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(
photoUrl,
height: 180,
fit: BoxFit.cover,
),
),
const SizedBox(height: 24),
const SectionTitle('4. Card variants'),
const SizedBox(height: 12),
const Card(
child: ListTile(
leading: CircleAvatar(
child: Icon(Icons.person),
),
title: Text('Elevated Card'),
subtitle: Text('Uses elevation and shadow.'),
),
),
const Card.filled(
child: ListTile(
leading: Icon(Icons.favorite),
title: Text('Filled Card'),
),
),
const Card.outlined(
child: ListTile(
leading: Icon(Icons.chat),
title: Text('Outlined Card'),
),
),
const SizedBox(height: 24),
const SectionTitle('5. Material and InkWell'),
const SizedBox(height: 12),
Material(
color: Colors.deepPurple,
borderRadius: BorderRadius.circular(16),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Profile opened'),
),
);
},
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
child: Row(
children: [
Icon(
Icons.person,
color: Colors.white,
),
SizedBox(width: 12),
Expanded(
child: Text(
'Open Profile',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
Icon(
Icons.chevron_right,
color: Colors.white,
),
],
),
),
),
),
const SizedBox(height: 24),
const SectionTitle('6. ClipOval'),
const SizedBox(height: 12),
Center(
child: ClipOval(
child: Image.network(
photoUrl,
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
),
const SizedBox(height: 32),
],
),
);
}
}
class SectionTitle extends StatelessWidget {
const SectionTitle(
this.text, {
super.key,
});
final String text;
@override
Widget build(BuildContext context) {
return Text(
text,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
);
}
}
三十三、Widget 选择对比表
| 需求 | 推荐 Widget | 原因 |
|---|---|---|
| 纯色背景 | ColoredBox | 职责明确 |
| 背景、渐变、边框 | DecoratedBox | 专门绘制 Decoration |
| 同时处理尺寸、间距和装饰 | Container | 使用方便 |
| Material 风格内容区域 | Card | 内置 Card 语义和主题 |
| 自定义 Material 表面 | Material | 支持 elevation 和 Ink |
| 矩形裁剪 | ClipRect | 适合普通矩形 |
| 圆角矩形裁剪 | ClipRRect | 适合圆角图片 |
| 圆形或椭圆裁剪 | ClipOval | 适合头像 |
| 自定义复杂形状裁剪 | ClipPath | 成本通常更高 |
| 点击水波纹 | Material + InkWell | 水波纹绘制在 Material 上 |
三十四、本章核心总结
- Container 是组合型便利组件,不是单一功能组件。
width和height会转换为 BoxConstraints。- Container 的宽高包含 Padding。
- Margin 位于 Decoration 外部,不会被背景绘制。
color是简单 BoxDecoration 颜色的快捷写法。color和decoration不能同时设置。- Decoration 绘制在 child 后面。
- foregroundDecoration 绘制在 child 前面。
- BoxDecoration 可以绘制背景、渐变、图片、边框和阴影。
borderRadius只负责绘制形状,不默认裁剪 child。- 普通图片圆角通常使用 ClipRRect。
- 纯色背景优先考虑 ColoredBox。
- 只需要装饰时可以使用 DecoratedBox。
- Card 是 Material 风格的内容容器。
- Material 负责 elevation、形状及 Ink 水波纹表面。
- 不透明 Container 可能遮挡 InkWell 水波纹。
- 只有 child 会超出形状时才需要裁剪。
Clip.antiAliasWithSaveLayer成本较高,应谨慎使用。