Flutter Base Widgets 第四章:单子组件容器

0 阅读16分钟

本章摘要

本章讲解 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 边框需要的空间计算进去;然后应用 widthheight 和额外约束;最后在外部加入 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 = 200maxWidth = 200

height: 100
→ minHeight = 100maxHeight = 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

widthheight 包含 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 的大小确定尺寸;widthheightconstraints 可以改变这一行为。


九、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。官方明确说明,shapeborderRadius 不会自动裁剪 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 不只是一个背景容器。

它主要提供:

  1. Material 形状和表面
  2. Elevation 和阴影
  3. Ink 水波纹的绘制区域
  4. 可选的形状裁剪

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


二十九、裁剪的性能问题

裁剪通常比完全不裁剪更贵,但普通 ClipRectClipRRect 不一定会自动触发昂贵的 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 上

三十四、本章核心总结

  1. Container 是组合型便利组件,不是单一功能组件。
  2. widthheight 会转换为 BoxConstraints。
  3. Container 的宽高包含 Padding。
  4. Margin 位于 Decoration 外部,不会被背景绘制。
  5. color 是简单 BoxDecoration 颜色的快捷写法。
  6. colordecoration 不能同时设置。
  7. Decoration 绘制在 child 后面。
  8. foregroundDecoration 绘制在 child 前面。
  9. BoxDecoration 可以绘制背景、渐变、图片、边框和阴影。
  10. borderRadius 只负责绘制形状,不默认裁剪 child。
  11. 普通图片圆角通常使用 ClipRRect。
  12. 纯色背景优先考虑 ColoredBox。
  13. 只需要装饰时可以使用 DecoratedBox。
  14. Card 是 Material 风格的内容容器。
  15. Material 负责 elevation、形状及 Ink 水波纹表面。
  16. 不透明 Container 可能遮挡 InkWell 水波纹。
  17. 只有 child 会超出形状时才需要裁剪。
  18. Clip.antiAliasWithSaveLayer 成本较高,应谨慎使用。