本章介绍:
- Center
- Align
- Alignment
- AlignmentDirectional
- FractionalOffset
- Stack
- Positioned
- Positioned.fill
- FractionalTranslation
- Transform
- Baseline
重点解决:
- Center 和 Align 有什么区别
Alignment(-1, 0, 1)分别表示什么- Alignment 和 FractionalOffset 如何计算位置
- Positioned 为什么只能放在 Stack 中
left、right、width为什么不能同时设置- 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 是否占满可用空间,取决于:
- 父组件传来的约束
widthFactorheightFactor
默认情况下:
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 不只支持 -1、0 和 1。
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
不表示右边缘,而是继续向右外推。
这种写法可能造成:
- 内容超出父组件
- 内容被裁剪
- 覆盖其他组件
- 点击区域难以理解
普通业务布局一般优先保持在 -1 到 1 之间。
五、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 的大小取决于父约束和 widthFactor、heightFactor。
默认情况
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
注意:
widthFactor和heightFactor改变的是 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。
当 left 和 right 都不为空时,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 | 注意事项 |
|---|---|---|
| 居中 child | Center | Align 的中心简化版 |
| 任意方向对齐 | Align | 支持 Alignment |
| 支持 RTL 的开始或结束位置 | AlignmentDirectional | 避免写死左右 |
| Stack 中固定边距定位 | Positioned | 必须位于 Stack 中 |
| 填满整个 Stack | Positioned.fill | 常用于遮罩和背景 |
| 按 child 自身比例移动 | FractionalTranslation | 不改变布局尺寸 |
| 固定像素平移 | Transform.translate | 不改变布局尺寸 |
| 视觉缩放 | Transform.scale | 可能覆盖或被裁剪 |
| 视觉旋转 | Transform.rotate | 角度单位是弧度 |
| 布局阶段旋转 | RotatedBox | 使用 quarterTurns |
| 不同字号文字对齐 | Baseline 或 Row baseline | 需要指定 TextBaseline |
| 多行文字与图标顶部对齐 | CrossAxisAlignment.start | 通常比基线更合适 |
三十三、本章核心总结
- 子组件的位置通常由父组件决定。
- Center 是固定使用
Alignment.center的 Align。 - Alignment 以中心为原点,范围通常是
-1到1。 - FractionalOffset 以左上角为原点,通常使用
0到1。 - 需要支持 RTL 时,优先使用 AlignmentDirectional。
- Positioned 只能在 Stack 的有效子树中使用。
- 水平方向的
left、right、width最多同时设置两个。 - Positioned.fill 适合背景、渐变和 Loading 遮罩。
- Align 强调对齐关系,Positioned 强调与边缘的距离。
- Transform 主要改变绘制结果,不改变布局占用空间。
- 真正需要增加间距时,应使用 Padding 或 SizedBox。
- FractionalTranslation 按 child 自身尺寸比例移动。
- Transform.translate 使用固定逻辑像素移动。
- Transform.rotate 的角度单位是弧度。
- 不同字号文字通常应使用基线对齐。