Flutter Base Widgets 第三章:对齐与定位

2 阅读18分钟

本章介绍:

  • Center
  • Align
  • Alignment
  • AlignmentDirectional
  • FractionalOffset
  • Stack
  • Positioned
  • Positioned.fill
  • FractionalTranslation
  • Transform
  • Baseline

重点解决:

  • Center 和 Align 有什么区别
  • Alignment(-1, 0, 1) 分别表示什么
  • Alignment 和 FractionalOffset 如何计算位置
  • Positioned 为什么只能放在 Stack 中
  • leftrightwidth 为什么不能同时设置
  • Transform 为什么移动了画面却没有改变布局空间
  • 固定像素移动和按自身比例移动有什么区别
  • 文字、数字和图标如何按基线对齐

一、再次理解定位规则

Flutter 的布局规则是:

Constraints go down, sizes go up, parents set positions.

即:

父组件向子组件传递约束
子组件决定自己的尺寸
子组件把尺寸返回给父组件
父组件决定子组件的位置

子组件通常不能直接决定自己在屏幕上的绝对位置。

例如:

Container(
  width: 100,
  height: 100,
);

Container 只能表达自己想要的尺寸,最终放在屏幕的左边、中央还是右边,通常由父组件决定。Flutter 的布局系统明确规定,父组件负责定位子组件。


二、Center

1. Center 的作用

Center 用于把一个 child 放在自身可用空间的中央。

Center(
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
  ),
);

如果 Center 收到整个屏幕的约束:

Center 占满屏幕
    ↓
给 child 相对宽松的约束
    ↓
child 选择 100 × 100
    ↓
Center 把 child 放在中央

2. Center 本质上是什么

Center 是 Align 的简化版本:

const Align(
  alignment: Alignment.center,
  child: child,
);

也就是说:

Center
=
alignment 固定为 Alignment.center 的 Align

Flutter 的 Align API 也将 Center 定义为 alignment 始终为中心的实现。


3. Center 不一定占满父组件

Center 是否占满可用空间,取决于:

  • 父组件传来的约束
  • widthFactor
  • heightFactor

默认情况下:

Center(
  child: child,
);

如果父组件提供有限最大尺寸,Center 通常尽可能使用这些空间。

但设置:

Center(
  widthFactor: 1,
  heightFactor: 1,
  child: child,
);

Center 会尝试根据 child 的尺寸计算自身大小:

Center 宽度 = child 宽度 × widthFactor
Center 高度 = child 高度 × heightFactor

4. widthFactor 和 heightFactor

Center(
  widthFactor: 2,
  heightFactor: 2,
  child: Container(
    width: 50,
    height: 50,
    color: Colors.blue,
  ),
);

在父约束允许的情况下:

Center 宽度约为 50 × 2 = 100
Center 高度约为 50 × 2 = 100

child 仍然显示为 50 × 50,只是 Center 自身的布局空间变大。


三、Align

1. Align 的作用

Align 用于把 child 放在自身空间中的指定位置。

Align(
  alignment: Alignment.bottomRight,
  child: Container(
    width: 80,
    height: 80,
    color: Colors.blue,
  ),
);

常见位置包括:

Alignment.topLeft
Alignment.topCenter
Alignment.topRight

Alignment.centerLeft
Alignment.center
Alignment.centerRight

Alignment.bottomLeft
Alignment.bottomCenter
Alignment.bottomRight

Align 会把 alignment 描述的 child 内部位置,与 Align 自身的相同 alignment 位置对齐。


2. Center 和 Align 的区别

Center

只能居中:

const Center(
  child: Text('Hello'),
);

Align

可以选择任意对齐位置:

const Align(
  alignment: Alignment.centerRight,
  child: Text('Hello'),
);

选择原则:

只需要居中
→ Center

需要顶部、底部、左侧、右侧或自定义位置
→ Align

四、Alignment 坐标系统

1. 基本坐标

Alignment 使用以中心为原点的坐标系统:

左上角   (-1, -1)
顶部     ( 0, -1)
右上角   ( 1, -1)

左侧     (-1,  0)
中心     ( 0,  0)
右侧     ( 1,  0)

左下角   (-1,  1)
底部     ( 0,  1)
右下角   ( 1,  1)

例如:

const Align(
  alignment: Alignment(-1, -1),
  child: Text('Top left'),
);

等价于:

const Align(
  alignment: Alignment.topLeft,
  child: Text('Top left'),
);

Flutter 中 Alignment(0, 0) 表示中心,Alignment(-1, -1) 表示左上角,Alignment(1, 1) 表示右下角。


2. x 和 y 的意义

Alignment(x, y)

其中:

x 控制水平方向
y 控制垂直方向

水平:

x = -1 → 左侧
x =  0 → 水平居中
x =  1 → 右侧

垂直:

y = -1 → 顶部
y =  0 → 垂直居中
y =  1 → 底部

3. 小数坐标

Alignment 不只支持 -101

const Align(
  alignment: Alignment(0.5, -0.5),
  child: Icon(Icons.favorite),
);

它表示:

水平方向:中心与右侧之间
垂直方向:顶部与中心之间

Alignment 会在线性坐标中插值。


4. 超出 -1 到 1

Alignment 也可以超过正常范围:

const Align(
  alignment: Alignment(2, 0),
  child: Text('Outside'),
);

这会把 child 推到正常右侧位置之外。

例如:

x = 2

不表示右边缘,而是继续向右外推。

这种写法可能造成:

  • 内容超出父组件
  • 内容被裁剪
  • 覆盖其他组件
  • 点击区域难以理解

普通业务布局一般优先保持在 -11 之间。


五、AlignmentDirectional

普通 Alignment 使用物理方向:

left
right

但有些界面需要根据语言方向自动变化。

例如:

const Align(
  alignment: AlignmentDirectional.centerStart,
  child: Text('Title'),
);

在从左到右语言中:

start = left

在阿拉伯语、希伯来语等从右到左语言中:

start = right

常用值:

AlignmentDirectional.topStart
AlignmentDirectional.centerStart
AlignmentDirectional.bottomStart

AlignmentDirectional.topEnd
AlignmentDirectional.centerEnd
AlignmentDirectional.bottomEnd

需要支持国际化时,业务中的“开始位置”和“结束位置”通常应该优先使用:

start / end

而不是写死:

left / right

Transform 的对齐原点同样支持依赖文字方向的 AlignmentDirectional


六、Alignment 与 FractionalOffset 的区别

两者都可以传给 Align:

Align(
  alignment: Alignment(0.2, 0.6),
  child: child,
);
Align(
  alignment: FractionalOffset(0.2, 0.6),
  child: child,
);

但坐标系统不同。


1. Alignment

Alignment 以中心为原点:

左上角 = (-1, -1)
中心   = ( 0,  0)
右下角 = ( 1,  1)

2. FractionalOffset

FractionalOffset 以左上角为原点:

左上角 = (0, 0)
中心   = (0.5, 0.5)
右下角 = (1, 1)

例如:

const Align(
  alignment: FractionalOffset(0.25, 0.75),
  child: Icon(Icons.circle),
);

大致表示:

水平方向位于 25%
垂直方向位于 75%

Flutter 官方说明,Alignment 使用以中心为原点的坐标,而 FractionalOffset 使用以左上角为原点、基于宽高比例的坐标。


3. 如何选择

需要使用 topLeft、center、bottomRight 等语义位置
→ Alignment

需要按照父组件宽高百分比理解位置
→ FractionalOffset

普通业务更常见的是:

Alignment.center
Alignment.topRight
AlignmentDirectional.centerStart

七、Align 是否会改变自身大小

Align 的大小取决于父约束和 widthFactorheightFactor

默认情况

Align(
  alignment: Alignment.bottomRight,
  child: child,
);

如果父组件提供有限空间,Align 通常尽可能使用可用空间,然后在其中定位 child。


使用尺寸因子

Align(
  widthFactor: 2,
  heightFactor: 1.5,
  alignment: Alignment.center,
  child: const Icon(
    Icons.favorite,
    size: 40,
  ),
);

在约束允许时:

Align 宽度约为 child 宽度 × 2
Align 高度约为 child 高度 × 1.5

注意:

widthFactorheightFactor 改变的是 Align 自身的尺寸,不是缩放 child。

要缩放 child,应使用:

Transform.scale
FittedBox

八、Stack 与定位

Positioned 依赖 Stack,因此先理解 Stack。

Stack(
  children: [
    Image.network(photoUrl),
    const Positioned(
      right: 8,
      top: 8,
      child: Icon(Icons.star),
    ),
  ],
);

Stack 允许多个 child 在同一块区域中重叠。

它按照 children 顺序绘制:

第一个 child 在最底层
后面的 child 依次覆盖前面的 child

Stack 会先根据非 Positioned 子组件确定自己的尺寸,然后把 Positioned 子组件放到指定位置。


九、Positioned

1. Positioned 的作用

Positioned 用于控制 Stack 中 child 相对 Stack 边缘的位置。

Stack(
  children: [
    Container(color: Colors.blue),
    const Positioned(
      right: 12,
      bottom: 12,
      child: Icon(
        Icons.favorite,
        color: Colors.white,
      ),
    ),
  ],
);

表示:

距离 Stack 右侧 12
距离 Stack 底部 12

Positioned 必须位于 Stack 的有效子树路径中。


2. 为什么 Positioned 只能放在 Stack 中

Positioned 本身不会普通地执行定位。

它会向最近的 Stack 写入特殊的 ParentData:

left
top
right
bottom
width
height

只有 Stack 的 RenderObject 知道如何读取和使用这些定位数据。

错误:

Column(
  children: [
    Positioned(
      top: 10,
      child: Text('Hello'),
    ),
  ],
);

可能出现:

Incorrect use of ParentDataWidget

正确:

Stack(
  children: [
    Positioned(
      top: 10,
      child: Text('Hello'),
    ),
  ],
);

3. Positioned 与 Stack 的路径要求

下面是正确的:

Stack(
  children: [
    Positioned(
      top: 10,
      child: UserBadge(),
    ),
  ],
);

中间可以存在普通 StatelessWidget 或 StatefulWidget 封装,但不能插入会建立不兼容 RenderObject ParentData 的组件。

最简单的原则是:

尽量让 Positioned 成为 Stack 的直接 child。


十、Positioned 的定位规则

1. left 和 top

Positioned(
  left: 20,
  top: 30,
  child: child,
);

表示:

child 左边距离 Stack 左边 20
child 顶边距离 Stack 顶边 30

child 的宽高根据自身内容决定。


2. right 和 bottom

Positioned(
  right: 20,
  bottom: 30,
  child: child,
);

表示:

child 右边距离 Stack 右边 20
child 底边距离 Stack 底边 30

3. left 和 right

Positioned(
  left: 16,
  right: 16,
  top: 20,
  child: child,
);

child 宽度会被强制为:

Stack 宽度 - 16 - 16

即左右各保留 16。

leftright 都不为空时,Positioned 会根据二者强制确定 child 的宽度。


4. top 和 bottom

Positioned(
  top: 16,
  bottom: 16,
  left: 20,
  child: child,
);

child 高度会被强制为:

Stack 高度 - 16 - 16

5. left 和 width

Positioned(
  left: 20,
  width: 120,
  top: 20,
  child: child,
);

表示:

距离左侧 20
固定宽度 120

6. right 和 width

Positioned(
  right: 20,
  width: 120,
  top: 20,
  child: child,
);

表示:

距离右侧 20
固定宽度 120

十一、为什么 left、right 和 width 不能同时设置

错误:

Positioned(
  left: 10,
  right: 10,
  width: 100,
  child: child,
);

这三个参数同时给出了两套互相冲突的宽度信息:

left + right
已经可以计算 child 宽度

width
又明确指定了一个宽度

Flutter 不知道应该以哪一种为准。

因此在水平方向:

left
right
width

最多只能同时设置两个。

垂直方向同理:

top
bottom
height

最多只能同时设置两个。Positioned 的构造函数对此有明确断言。


十二、Positioned.fill

1. 填满整个 Stack

Positioned.fill(
  child: ColoredBox(
    color: Colors.black26,
  ),
);

默认等价于:

Positioned(
  left: 0,
  top: 0,
  right: 0,
  bottom: 0,
  child: child,
);

适合:

  • Loading 遮罩
  • 图片渐变层
  • 点击遮罩
  • 全区域背景
  • 半透明蒙层

2. 页面 Loading 遮罩

Stack(
  children: [
    const PageContent(),
    if (isLoading)
      const Positioned.fill(
        child: ColoredBox(
          color: Color(0x66000000),
          child: Center(
            child: CircularProgressIndicator(),
          ),
        ),
      ),
  ],
);

Loading 会覆盖整个 Stack。


3. 底部渐变层

Stack(
  children: [
    Positioned.fill(
      child: Image.network(
        photoUrl,
        fit: BoxFit.cover,
      ),
    ),
    const Positioned.fill(
      child: DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Colors.transparent,
              Colors.black87,
            ],
          ),
        ),
      ),
    ),
    const Positioned(
      left: 16,
      right: 16,
      bottom: 16,
      child: Text(
        'Alice, 32',
        style: TextStyle(
          color: Colors.white,
          fontSize: 24,
          fontWeight: FontWeight.bold,
        ),
      ),
    ),
  ],
);

十三、Positioned 与 Align 的区别

Positioned

基于 Stack 边缘的具体距离:

Positioned(
  right: 12,
  bottom: 12,
  child: child,
);

适合:

  • 距离右上角固定 8
  • 距离底部固定 16
  • 左右边距固定
  • 明确的覆盖位置

Align

基于 Stack 或父组件整体空间对齐:

Align(
  alignment: Alignment.bottomRight,
  child: child,
);

适合:

  • 居中
  • 右下角
  • 顶部中央
  • 按比例理解位置
  • 不需要固定边距

Align 加 Padding

Align(
  alignment: Alignment.bottomRight,
  child: Padding(
    padding: const EdgeInsets.all(12),
    child: child,
  ),
);

视觉上可能与:

Positioned(
  right: 12,
  bottom: 12,
  child: child,
);

相同。

选择原则:

强调对齐关系
→ Align

强调距离 Stack 边缘的固定值
→ Positioned

十四、FractionalTranslation

1. 作用

FractionalTranslation 按照 child 自身尺寸的比例移动绘制位置。

FractionalTranslation(
  translation: const Offset(0.5, 0),
  child: child,
);

表示:

向右移动 child 自身宽度的 50%

Flutter 官方定义中,Offset(0.25, 0) 表示向右移动 child 宽度的四分之一。


2. translation 参数

Offset(dx, dy)

其中:

dx 按 child 宽度计算
dy 按 child 高度计算

例如 child 尺寸为:

宽 200
高 100

使用:

const FractionalTranslation(
  translation: Offset(0.25, -0.5),
  child: child,
);

实际移动约为:

水平方向:200 × 0.25 = 向右 50
垂直方向:100 × -0.5 = 向上 50

3. 常见使用场景

让徽章向外偏移一半

Stack(
  clipBehavior: Clip.none,
  children: [
    const CircleAvatar(
      radius: 40,
    ),
    Positioned(
      right: 0,
      top: 0,
      child: FractionalTranslation(
        translation: const Offset(0.5, -0.5),
        child: Container(
          width: 20,
          height: 20,
          decoration: const BoxDecoration(
            color: Colors.red,
            shape: BoxShape.circle,
          ),
        ),
      ),
    ),
  ],
);

红点会以自身宽高为基准:

向右移动一半宽度
向上移动一半高度

这样红点中心可以更准确地压在头像边缘上。


十五、Transform.translate 与 FractionalTranslation

Transform.translate

使用固定逻辑像素:

Transform.translate(
  offset: const Offset(20, -10),
  child: child,
);

表示:

向右 20
向上 10

与 child 尺寸无关。


FractionalTranslation

使用 child 自身尺寸比例:

FractionalTranslation(
  translation: const Offset(0.5, -0.5),
  child: child,
);

表示:

向右移动自身宽度的 50%
向上移动自身高度的 50%

选择原则:

要求固定像素距离
→ Transform.translate

要求跟随 child 尺寸变化
→ FractionalTranslation

十六、Transform

1. Transform 的作用

Transform 可以在绘制 child 前应用变换,例如:

  • 平移
  • 缩放
  • 旋转
  • 翻转
  • 倾斜
  • Matrix4 自定义变换

最重要的特点是:

Transform 默认只改变绘制结果,不改变布局阶段占用的空间。

Flutter 官方说明,Transform 在绘制前应用变换,变换不会被计入 child 的布局尺寸。


十七、Transform.translate

Transform.translate(
  offset: const Offset(40, 20),
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
  ),
);

视觉上蓝色方块会:

向右移动 40
向下移动 20

但布局系统仍然认为它占据原来的 100 × 100 位置。


Transform 为什么可能覆盖其他组件

Column(
  children: [
    Transform.translate(
      offset: const Offset(0, 50),
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
  ],
);

Column 布局时仍然按照蓝色方块的原始位置计算。

绘制时蓝色方块向下移动 50,可能覆盖红色方块。

结构可以理解为:

布局位置:没有移动
绘制画面:移动了

十八、需要真正改变布局位置怎么办

不要使用 Transform 来代替正常布局间距。

错误:

Transform.translate(
  offset: const Offset(0, 20),
  child: child,
);

如果真正需求是给 child 增加顶部间距,应使用:

Padding(
  padding: const EdgeInsets.only(top: 20),
  child: child,
);

或者:

Column(
  children: [
    const SizedBox(height: 20),
    child,
  ],
);

选择原则:

希望其他组件也知道这个位置变化
→ 使用 Padding、Align、Positioned 等布局组件

只希望视觉效果移动
→ Transform.translate

十九、Transform.scale

Transform.scale(
  scale: 1.2,
  child: child,
);

表示视觉上放大到 1.2 倍。

也可以分别设置:

Transform.scale(
  scaleX: 1.2,
  scaleY: 0.8,
  child: child,
);

注意:

布局占用空间仍按照原始尺寸计算

放大后的内容可能:

  • 超出父组件
  • 被裁剪
  • 覆盖附近组件

二十、Transform.rotate

import 'dart:math' as math;

Transform.rotate(
  angle: math.pi / 4,
  child: child,
);

角度使用弧度:

180° = π
90°  = π / 2
45°  = π / 4

Transform.rotate 默认围绕中心旋转。


alignment 控制旋转中心

Transform.rotate(
  angle: math.pi / 4,
  alignment: Alignment.topLeft,
  child: child,
);

表示围绕 child 左上角旋转。

常见:

alignment: Alignment.center
alignment: Alignment.topLeft
alignment: Alignment.bottomRight

origin

Transform.rotate(
  angle: math.pi / 4,
  origin: const Offset(50, 50),
  child: child,
);

origin 会在 alignment 确定的原点基础上进一步添加偏移。

普通旋转通常优先使用:

alignment

自定义复杂旋转轴时再考虑:

origin

二十一、Transform.flip

Transform.flip(
  flipX: true,
  child: child,
);

表示水平镜像。

Transform.flip(
  flipY: true,
  child: child,
);

表示垂直镜像。

如果两者都是 true,相当于同时水平和垂直翻转。

适合:

  • 镜像图标
  • 前置摄像头预览
  • 需要反转方向的装饰图
  • 特殊动画效果

二十二、Transform 与 RotatedBox 的区别

Transform.rotate

Transform.rotate(
  angle: math.pi / 2,
  child: child,
);

只改变绘制结果:

布局尺寸仍是旋转前尺寸

RotatedBox

RotatedBox(
  quarterTurns: 1,
  child: child,
);

在布局阶段旋转:

旋转后的宽高会参与布局

例如一个:

宽 200
高 50

的 child 旋转 90°:

Transform.rotate
布局仍可能认为是 200 × 50

RotatedBox
布局按约 50 × 200 处理

Flutter 官方也明确区分:Transform 在绘制阶段旋转,RotatedBox 在布局前旋转。


二十三、transformHitTests

Transform 默认:

transformHitTests: true

表示点击测试会跟随变换后的视觉位置。

Transform.translate(
  offset: const Offset(100, 0),
  transformHitTests: true,
  child: GestureDetector(
    onTap: handleTap,
    child: child,
  ),
);

用户通常可以点击移动后的 child。

如果设置:

transformHitTests: false

视觉位置变了,但点击区域不会按照相同方式转换,容易出现:

看得到却点不到
或者
原位置仍能触发

除非有特殊需求,一般保持默认值。


二十四、FractionalTranslation 的点击边界

FractionalTranslation 也是绘制阶段的移动。

即使 child 被移动到原边界之外,超出 FractionalTranslation 自身布局边界的部分不一定都能正常参与点击测试。Flutter 官方特别指出,点击检测仍受到 FractionalTranslation 自身边界影响。

因此不要把重要按钮大幅移动到父布局区域之外。

更可靠的方式是:

  • 扩大父组件布局尺寸
  • 使用 Stack 和 Positioned
  • 使用 Padding
  • 调整真实布局结构

二十五、Baseline

1. Baseline 的作用

Baseline 用于根据 child 的文字基线定位 child。

Baseline(
  baseline: 40,
  baselineType: TextBaseline.alphabetic,
  child: const Text(
    'Hello',
    style: TextStyle(fontSize: 24),
  ),
);

表示:

让文字基线距离当前 Baseline Widget 顶部 40

如果 child 没有基线,Flutter 会使用 child 底部作为参考。


二十六、什么是文字基线

文字并不是简单地以外框底部对齐。

例如:

Alice
123
gjpq

字母:

g
j
p
q

会有下降部分。

文字排版通常依靠基线对齐,而不是外框顶部或底部对齐。

简化理解:

       A B C
──────────────  alphabetic baseline
         g p q

使用基线可以让不同字号的文字看起来处于同一条文字线上。


二十七、TextBaseline 类型

常见类型:

TextBaseline.alphabetic
TextBaseline.ideographic

alphabetic

适合以拉丁字母为主的文字:

English
数字
大多数西文内容

ideographic

适合表意文字相关排版,例如:

中文
日文
部分东亚文字排版

实际业务中,如果主要是英文 UI,一般使用:

TextBaseline.alphabetic

二十八、不同字号文字对齐

错误示例:

Row(
  crossAxisAlignment: CrossAxisAlignment.center,
  children: const [
    Text(
      '$',
      style: TextStyle(fontSize: 18),
    ),
    Text(
      '99',
      style: TextStyle(fontSize: 42),
    ),
    Text(
      '/month',
      style: TextStyle(fontSize: 14),
    ),
  ],
);

问题表现

虽然组件外框垂直居中,但文字底部和视觉基线看起来不整齐。

正确写法

Row(
  crossAxisAlignment: CrossAxisAlignment.baseline,
  textBaseline: TextBaseline.alphabetic,
  children: const [
    Text(
      '$',
      style: TextStyle(fontSize: 18),
    ),
    Text(
      '99',
      style: TextStyle(
        fontSize: 42,
        fontWeight: FontWeight.bold,
      ),
    ),
    Text(
      '/month',
      style: TextStyle(fontSize: 14),
    ),
  ],
);

在 Row 中通常比单独使用多个 Baseline 更方便。


二十九、图标与文字垂直对齐

错误:

Row(
  children: const [
    Icon(Icons.location_on),
    Text(
      'Los Angeles\nCalifornia',
    ),
  ],
);

默认 CrossAxisAlignment.center 时,图标会相对整段两行文字居中。

但需求可能是图标与第一行顶部对齐。

正确:

Row(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: const [
    Padding(
      padding: EdgeInsets.only(top: 2),
      child: Icon(
        Icons.location_on,
        size: 20,
      ),
    ),
    SizedBox(width: 6),
    Expanded(
      child: Text(
        'Los Angeles\nCalifornia',
      ),
    ),
  ],
);

注意:

图标字体和文字字体不一定共享相同的理想基线。

因此图标和多行文字经常更适合使用:

CrossAxisAlignment.start

再通过少量 Padding 做视觉校正。


三十、常见错误与正确写法

错误一:用 Center 实现右下角对齐

错误示例

Center(
  child: child,
);

问题

Center 只能居中。

正确写法

Align(
  alignment: Alignment.bottomRight,
  child: child,
);

错误二:Positioned 放在 Column 中

错误示例

Column(
  children: [
    Positioned(
      top: 10,
      child: child,
    ),
  ],
);

问题表现

出现 ParentDataWidget 错误。

正确写法

Stack(
  children: [
    Positioned(
      top: 10,
      child: child,
    ),
  ],
);

错误三:同时设置 left、right 和 width

错误示例

Positioned(
  left: 16,
  right: 16,
  width: 200,
  child: child,
);

原因

宽度信息冲突。

正确写法一:左右拉伸

Positioned(
  left: 16,
  right: 16,
  child: child,
);

正确写法二:固定宽度

Positioned(
  left: 16,
  width: 200,
  child: child,
);

错误四:使用 Transform 代替真实间距

错误示例

Transform.translate(
  offset: const Offset(0, 20),
  child: child,
);

问题表现

child 视觉位置变了,但下面组件位置没有改变,可能发生覆盖。

正确写法

Padding(
  padding: const EdgeInsets.only(top: 20),
  child: child,
);

错误五:放大后内容被裁剪

错误示例

SizedBox(
  width: 100,
  height: 100,
  child: Transform.scale(
    scale: 1.5,
    child: child,
  ),
);

问题

Transform 不会增加布局空间,祖先组件可能裁剪超出部分。

正确处理

根据真实需求:

SizedBox(
  width: 150,
  height: 150,
  child: Transform.scale(
    scale: 1.5,
    child: child,
  ),
);

或者调整父组件:

clipBehavior: Clip.none

但即使取消裁剪,也要防止覆盖其他内容。


错误六:旋转角度直接填写 90

错误示例

Transform.rotate(
  angle: 90,
  child: child,
);

原因

angle 使用弧度,不是角度。

正确写法

Transform.rotate(
  angle: math.pi / 2,
  child: child,
);

错误七:用 FractionalTranslation 表示固定 20 像素

错误示例

FractionalTranslation(
  translation: const Offset(0.2, 0),
  child: child,
);

这表示自身宽度的 20%,不是 20 像素。

正确写法

Transform.translate(
  offset: const Offset(20, 0),
  child: child,
);

错误八:Row 使用 baseline 但没有 textBaseline

错误示例

Row(
  crossAxisAlignment: CrossAxisAlignment.baseline,
  children: children,
);

问题表现

Flutter 报错,因为不知道使用哪种文字基线。

正确写法

Row(
  crossAxisAlignment: CrossAxisAlignment.baseline,
  textBaseline: TextBaseline.alphabetic,
  children: children,
);

三十一、完整可运行示例

import 'dart:math' as math;

import 'package:flutter/material.dart';

void main() {
  runApp(const AlignmentDemoApp());
}

class AlignmentDemoApp extends StatelessWidget {
  const AlignmentDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
        ),
        useMaterial3: true,
      ),
      home: const AlignmentDemoPage(),
    );
  }
}

class AlignmentDemoPage extends StatelessWidget {
  const AlignmentDemoPage({super.key});

  static const String 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('Alignment and Positioning'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const SectionTitle('1. Align'),
          const SizedBox(height: 12),
          Container(
            height: 140,
            color: Colors.blue.shade50,
            child: const Align(
              alignment: Alignment.bottomRight,
              child: Padding(
                padding: EdgeInsets.all(12),
                child: FlutterLogo(size: 56),
              ),
            ),
          ),
          const SizedBox(height: 24),

          const SectionTitle('2. Stack and Positioned'),
          const SizedBox(height: 12),
          Center(
            child: Stack(
              clipBehavior: Clip.none,
              children: [
                ClipRRect(
                  borderRadius: BorderRadius.circular(60),
                  child: Image.network(
                    photoUrl,
                    width: 120,
                    height: 120,
                    fit: BoxFit.cover,
                  ),
                ),
                Positioned(
                  right: 0,
                  bottom: 0,
                  child: FractionalTranslation(
                    translation: const Offset(0.2, 0.2),
                    child: Container(
                      width: 28,
                      height: 28,
                      decoration: BoxDecoration(
                        color: Colors.green,
                        shape: BoxShape.circle,
                        border: Border.all(
                          color: Colors.white,
                          width: 3,
                        ),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),

          const SectionTitle('3. Positioned.fill'),
          const SizedBox(height: 12),
          SizedBox(
            height: 180,
            child: Stack(
              children: [
                Positioned.fill(
                  child: Image.network(
                    photoUrl,
                    fit: BoxFit.cover,
                  ),
                ),
                const Positioned.fill(
                  child: DecoratedBox(
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                        begin: Alignment.topCenter,
                        end: Alignment.bottomCenter,
                        colors: [
                          Colors.transparent,
                          Colors.black87,
                        ],
                      ),
                    ),
                  ),
                ),
                const Positioned(
                  left: 16,
                  right: 16,
                  bottom: 16,
                  child: Text(
                    'Alice, 32',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),

          const SectionTitle('4. Transform'),
          const SizedBox(height: 12),
          SizedBox(
            height: 120,
            child: Center(
              child: Transform.rotate(
                angle: -math.pi / 12,
                child: Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 24,
                    vertical: 12,
                  ),
                  color: Colors.orange,
                  child: const Text(
                    'FEATURED',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
            ),
          ),
          const SizedBox(height: 24),

          const SectionTitle('5. Baseline'),
          const SizedBox(height: 12),
          const Row(
            crossAxisAlignment: CrossAxisAlignment.baseline,
            textBaseline: TextBaseline.alphabetic,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                '$',
                style: TextStyle(fontSize: 20),
              ),
              Text(
                '99',
                style: TextStyle(
                  fontSize: 48,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Text(
                '/month',
                style: TextStyle(fontSize: 16),
              ),
            ],
          ),
          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注意事项
居中 childCenterAlign 的中心简化版
任意方向对齐Align支持 Alignment
支持 RTL 的开始或结束位置AlignmentDirectional避免写死左右
Stack 中固定边距定位Positioned必须位于 Stack 中
填满整个 StackPositioned.fill常用于遮罩和背景
按 child 自身比例移动FractionalTranslation不改变布局尺寸
固定像素平移Transform.translate不改变布局尺寸
视觉缩放Transform.scale可能覆盖或被裁剪
视觉旋转Transform.rotate角度单位是弧度
布局阶段旋转RotatedBox使用 quarterTurns
不同字号文字对齐Baseline 或 Row baseline需要指定 TextBaseline
多行文字与图标顶部对齐CrossAxisAlignment.start通常比基线更合适

三十三、本章核心总结

  1. 子组件的位置通常由父组件决定。
  2. Center 是固定使用 Alignment.center 的 Align。
  3. Alignment 以中心为原点,范围通常是 -11
  4. FractionalOffset 以左上角为原点,通常使用 01
  5. 需要支持 RTL 时,优先使用 AlignmentDirectional。
  6. Positioned 只能在 Stack 的有效子树中使用。
  7. 水平方向的 leftrightwidth 最多同时设置两个。
  8. Positioned.fill 适合背景、渐变和 Loading 遮罩。
  9. Align 强调对齐关系,Positioned 强调与边缘的距离。
  10. Transform 主要改变绘制结果,不改变布局占用空间。
  11. 真正需要增加间距时,应使用 Padding 或 SizedBox。
  12. FractionalTranslation 按 child 自身尺寸比例移动。
  13. Transform.translate 使用固定逻辑像素移动。
  14. Transform.rotate 的角度单位是弧度。
  15. 不同字号文字通常应使用基线对齐。