5.3 变换(Transform)
📚 章节概览
本节学习 Transform 变换组件,它可以在绘制时对子组件应用矩阵变换:
- Transform - 变换组件
- Matrix4 - 4D变换矩阵
- translate - 平移
- rotate - 旋转
- scale - 缩放
- skew - 倾斜
- Transform注意事项 - 绘制阶段vs布局阶段
- RotatedBox - 布局阶段旋转
- 组合变换 - 多种变换组合
🎯 核心知识点
Transform工作原理
┌─────────────────────────────────┐
│ Flutter渲染流程 │
├─────────────────────────────────┤
│ 1. Build(构建) │
│ 2. Layout(布局) ← RotatedBox │
│ 3. Paint(绘制) ← Transform │
└─────────────────────────────────┘
关键区别:
Transform:在绘制阶段应用变换,不影响布局RotatedBox:在布局阶段应用旋转,影响布局
1️⃣ Transform(变换组件)
1.1 构造函数
Transform({
Key? key,
required Matrix4 transform,
Offset? origin,
AlignmentGeometry? alignment,
bool transformHitTests = true,
FilterQuality? filterQuality,
Widget? child,
})
1.2 主要属性
| 属性 | 类型 | 说明 |
|---|---|---|
transform | Matrix4 | 变换矩阵(必需) |
alignment | AlignmentGeometry? | 变换原点的对齐方式 |
origin | Offset? | 变换原点坐标 |
transformHitTests | bool | 是否变换触摸事件 |
child | Widget? | 子组件 |
2️⃣ 平移(Translate)
2.1 Transform.translate
Transform.translate({
Key? key,
required Offset offset,
bool transformHitTests = true,
FilterQuality? filterQuality,
Widget? child,
})
参数:
offset:平移偏移量(x, y)- 正值:向右/向下移动
- 负值:向左/向上移动
2.2 示例
// 书中示例:左移20像素,上移5像素
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.translate(
offset: Offset(-20.0, -5.0),
child: Text("Hello world"),
),
)
效果:
原点 ─────────────────►
│
│ ┌────────────┐ ← 原始位置(红色背景)
│ │ │
│ └────────────┘
│
│ ┌────────────┐ ← 平移后位置(文字)
│ │ Hello world│
│ └────────────┘
▼
2.3 坐标系统
// 坐标原点在左上角
Offset(0, 0) // 原点
Offset(10, 0) // 向右10像素
Offset(-10, 0) // 向左10像素
Offset(0, 10) // 向下10像素
Offset(0, -10) // 向上10像素
3️⃣ 旋转(Rotate)
3.1 Transform.rotate
Transform.rotate({
Key? key,
required double angle,
Offset? origin,
AlignmentGeometry? alignment = Alignment.center,
bool transformHitTests = true,
FilterQuality? filterQuality,
Widget? child,
})
参数:
angle:旋转角度(单位:弧度)alignment:旋转中心(默认为组件中心)
3.2 弧度与角度转换
import 'dart:math' as math;
// 角度 → 弧度
double radians = degrees * math.pi / 180;
// 常用角度
math.pi / 2 // 90度
math.pi // 180度
math.pi * 3 / 2 // 270度
math.pi * 2 // 360度
3.3 示例
// 书中示例:旋转90度
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.rotate(
angle: math.pi / 2, // 旋转90度
child: Text("Hello world"),
),
)
⚠️ 注意: 需要导入 dart:math 库
import 'dart:math' as math;
4️⃣ 缩放(Scale)
4.1 Transform.scale
Transform.scale({
Key? key,
double? scale,
double? scaleX,
double? scaleY,
Offset? origin,
AlignmentGeometry? alignment = Alignment.center,
bool transformHitTests = true,
FilterQuality? filterQuality,
Widget? child,
})
参数:
scale:统一缩放比例scaleX:X轴缩放比例scaleY:Y轴缩放比例
4.2 缩放值说明
| 值 | 效果 |
|---|---|
< 1.0 | 缩小 |
1.0 | 不变 |
> 1.0 | 放大 |
0.5 | 缩小到一半 |
2.0 | 放大到两倍 |
4.3 示例
// 书中示例:放大到1.5倍
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.scale(
scale: 1.5,
child: Text("Hello world"),
),
)
// 分别设置X、Y轴缩放
Transform.scale(
scaleX: 2.0, // X轴放大2倍
scaleY: 0.5, // Y轴缩小到一半
child: Text("Hello world"),
)
5️⃣ 倾斜(Skew)
5.1 Matrix4.skew
倾斜变换需要使用 Matrix4:
// 沿X轴倾斜
Matrix4.skewX(double alpha)
// 沿Y轴倾斜
Matrix4.skewY(double alpha)
// 同时倾斜X、Y轴
Matrix4.skew(double alpha, double beta)
参数: alpha/beta 为倾斜角度(单位:弧度)
5.2 示例
// 书中示例:沿Y轴倾斜0.3弧度
Container(
color: Colors.black,
child: Transform(
alignment: Alignment.topRight,
transform: Matrix4.skewY(0.3),
child: Container(
padding: EdgeInsets.all(8.0),
color: Colors.deepOrange,
child: Text('Apartment for rent!'),
),
),
)
效果:
┌─────────────┐ ┌──────────────┐
│ 原始形状 │ → ╱ 倾斜后形状 ╱
└─────────────┘ └──────────────┘
6️⃣ Transform注意事项
6.1 不影响布局空间
重要: Transform 只在绘制阶段应用,不改变组件的布局空间和位置!
示例:文字重叠问题
// 书中示例:缩放后文字重叠
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.scale(
scale: 1.5, // 放大1.5倍
child: Text("Hello world"),
),
),
Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0)),
],
)
问题分析:
┌─────────────────────────────┐
│ 布局空间(红色,未变化) │
│ ┌──────┐ │
│ │Hello │ ← 实际布局空间 │
│ └──────┘ │
│ │
│ ┌────────────┐ │
│ │Hello world │ ← 绘制时放大 │
│ └────────────┘ │
│ 你好 ← 紧挨红色区域 │
└─────────────────────────────┘
↑
文字重叠!
6.2 性能优势
由于 Transform 只作用于绘制阶段:
- ✅ 不需要重新布局(layout)
- ✅ 不需要重新构建(build)
- ✅ 性能好,适合动画
- ✅ Flutter动画组件大量使用
应用场景:
- 动画效果
- 视觉调整
- 性能敏感的场景
7️⃣ RotatedBox vs Transform.rotate
7.1 核心区别
| 特性 | Transform.rotate | RotatedBox |
|---|---|---|
| 作用阶段 | 绘制阶段(Paint) | 布局阶段(Layout) |
| 影响布局 | ❌ 不影响 | ✅ 影响 |
| 占用空间 | 原始大小 | 旋转后大小 |
| 旋转角度 | 任意角度 | 90度的倍数 |
| 参数单位 | 弧度 | 四分之一圈 |
7.2 Transform.rotate示例
// 书中示例
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.rotate(
angle: math.pi / 2, // 旋转90度
child: Text("Hello world"),
),
),
Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0)),
],
)
效果:
- 红色区域:原始布局空间(未旋转)
- 文字:绘制时旋转90度
- 可能与"你好"重叠
7.3 RotatedBox示例
// 书中示例(改用RotatedBox)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: RotatedBox(
quarterTurns: 1, // 旋转90度(1/4圈)
child: Text("Hello world"),
),
),
Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0)),
],
)
效果:
- 红色区域:旋转后的实际布局空间
- 子组件:旋转90度
- 不会重叠
7.4 RotatedBox参数
RotatedBox({
Key? key,
required int quarterTurns, // 旋转的1/4圈数
Widget? child,
})
quarterTurns值:
0:不旋转1:旋转90度(顺时针)2:旋转180度3:旋转270度4:旋转360度(回到原位)-1:旋转-90度(逆时针)
8️⃣ 组合变换
8.1 思考题:顺序很重要!
问题: 先平移后旋转 vs 先旋转后平移,效果一样吗?
答案: ❌ 不一样!
先平移后旋转
Transform.rotate(
angle: math.pi / 4,
child: Transform.translate(
offset: Offset(30, 0),
child: Container(...),
),
)
过程:
- 在原坐标系上向右平移30像素
- 然后旋转45度(连同坐标系一起旋转)
先旋转后平移
Transform.translate(
offset: Offset(30, 0),
child: Transform.rotate(
angle: math.pi / 4,
child: Container(...),
),
)
过程:
- 先旋转45度(坐标系也旋转)
- 在新坐标系上向右平移30像素
图解:
原坐标系:
Y
↑
│
└───→ X
先平移后旋转:
1. 平移 → (30, 0)
2. 旋转 → 绕原点旋转
先旋转后平移:
1. 旋转 → 坐标系旋转45°
2. 平移 → 沿新X轴平移
8.2 使用Matrix4组合
Transform(
transform: Matrix4.identity()
..translate(10.0, 20.0) // 平移
..rotateZ(0.5) // 旋转
..scale(1.5), // 缩放
child: Container(...),
)
链式调用:
..是Dart的级联操作符- 按顺序应用变换
9️⃣ 3D变换
9.1 Matrix4支持3D变换
// 绕X轴旋转
Matrix4.identity()..rotateX(angle)
// 绕Y轴旋转
Matrix4.identity()..rotateY(angle)
// 绕Z轴旋转
Matrix4.identity()..rotateZ(angle)
9.2 透视效果
Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001) // 设置透视
..rotateX(0.6), // X轴旋转
alignment: Alignment.center,
child: Container(
width: 150,
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.orange, Colors.red],
),
),
child: Center(child: Text('3D效果')),
),
)
setEntry参数:
setEntry(3, 2, value):设置透视值- 值越大,透视效果越明显
- 典型值:
0.001到0.003
🤔 常见问题(FAQ)
Q1: Transform和Container.transform有什么区别?
A: 功能相同,只是用法不同
// 方式1:Transform组件
Transform.rotate(
angle: math.pi / 4,
child: Container(...),
)
// 方式2:Container.transform
Container(
transform: Matrix4.rotationZ(math.pi / 4),
child: ...,
)
Q2: 为什么Transform不影响布局?
A: 因为它在绘制阶段执行,而布局已经完成
Flutter渲染流程:
1. Build(构建Widget树)
2. Layout(计算大小和位置) ← 布局完成
3. Paint(绘制) ← Transform在这里执行
Q3: 如何让Transform影响布局?
A: 有以下方法:
- 使用RotatedBox(仅旋转)
RotatedBox(
quarterTurns: 1,
child: Text("Hello"),
)
- 使用LayoutBuilder动态计算
LayoutBuilder(
builder: (context, constraints) {
return Container(
width: constraints.maxWidth * 1.5, // 手动调整布局
child: Transform.scale(
scale: 1.5,
child: Text("Hello"),
),
);
},
)
Q4: 如何实现卡片翻转动画?
A: 使用AnimationController + Transform
class FlipCard extends StatefulWidget {
@override
_FlipCardState createState() => _FlipCardState();
}
class _FlipCardState extends State<FlipCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001) // 透视
..rotateY(_controller.value * math.pi), // 旋转
alignment: Alignment.center,
child: Container(
width: 200,
height: 300,
color: Colors.blue,
child: Center(child: Text('卡片')),
),
);
},
);
}
}
Q5: Transform会影响触摸事件吗?
A: 默认会,可以通过 transformHitTests 控制
Transform.scale(
scale: 2.0,
transformHitTests: true, // 默认:变换触摸事件
child: GestureDetector(
onTap: () => print('Tapped'),
child: Container(
width: 50,
height: 50,
color: Colors.red,
),
),
)
// transformHitTests: false
// 触摸事件不会随Transform变换
🎯 跟着做练习
练习1:实现旋转动画按钮
目标: 点击按钮时360度旋转
要求:
- 使用Transform.rotate
- AnimationController控制动画
- 点击触发旋转
💡 查看答案
class RotatingButton extends StatefulWidget {
@override
_RotatingButtonState createState() => _RotatingButtonState();
}
class _RotatingButtonState extends State<RotatingButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _rotate() {
_controller.reset();
_controller.forward();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _rotate,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * math.pi,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'点击旋转',
style: TextStyle(color: Colors.white),
),
),
);
},
),
);
}
}
练习2:实现脉冲动画
目标: 组件循环放大缩小
要求:
- 使用Transform.scale
- 循环动画
- 从1.0到1.2再回到1.0
💡 查看答案
class PulseAnimation extends StatefulWidget {
final Widget child;
const PulseAnimation({Key? key, required this.child}) : super(key: key);
@override
_PulseAnimationState createState() => _PulseAnimationState();
}
class _PulseAnimationState extends State<PulseAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this,
)..repeat(reverse: true); // 循环并反向
_animation = Tween<double>(
begin: 1.0,
end: 1.2,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: widget.child,
);
},
);
}
}
// 使用
PulseAnimation(
child: Icon(
Icons.favorite,
size: 50,
color: Colors.red,
),
)
练习3:实现视差滚动效果
目标: 根据滚动位置应用不同的Transform
要求:
- 使用ListView
- Transform.translate实现视差
- 不同层级移动速度不同
💡 查看答案
class ParallaxScroll extends StatefulWidget {
@override
_ParallaxScrollState createState() => _ParallaxScrollState();
}
class _ParallaxScrollState extends State<ParallaxScroll> {
final ScrollController _controller = ScrollController();
double _offset = 0;
@override
void initState() {
super.initState();
_controller.addListener(() {
setState(() {
_offset = _controller.offset;
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListView(
controller: _controller,
children: [
// 背景层(移动慢)
Transform.translate(
offset: Offset(0, _offset * 0.3),
child: Container(
height: 300,
color: Colors.blue[100],
child: Center(child: Text('背景层')),
),
),
// 中间层(正常速度)
Container(
height: 300,
color: Colors.green[100],
child: Center(child: Text('中间层')),
),
// 前景层(移动快)
Transform.translate(
offset: Offset(0, -_offset * 0.3),
child: Container(
height: 300,
color: Colors.orange[100],
child: Center(child: Text('前景层')),
),
),
// 其他内容
...List.generate(10, (index) {
return Container(
height: 100,
color: index.isEven ? Colors.grey[200] : Colors.grey[300],
child: Center(child: Text('项目 $index')),
);
}),
],
);
}
}
📊 Transform方法速查表
| 方法 | 参数 | 说明 | 示例 |
|---|---|---|---|
| Transform.translate | offset | 平移 | Offset(10, 20) |
| Transform.rotate | angle | 旋转 | math.pi / 2 |
| Transform.scale | scale | 缩放 | 1.5 |
| Transform.scale | scaleX, scaleY | 分轴缩放 | scaleX: 2.0 |
| Matrix4.skewX | alpha | X轴倾斜 | 0.3 |
| Matrix4.skewY | alpha | Y轴倾斜 | 0.3 |
| Matrix4.rotateX | angle | 绕X轴旋转 | 0.5 |
| Matrix4.rotateY | angle | 绕Y轴旋转 | 0.5 |
| Matrix4.rotateZ | angle | 绕Z轴旋转 | 0.5 |
📋 小结
核心概念
// Transform:绘制阶段变换,不影响布局
Transform.translate(offset: Offset(10, 0), child: ...)
Transform.rotate(angle: math.pi / 4, child: ...)
Transform.scale(scale: 1.5, child: ...)
Transform(transform: Matrix4.skewX(0.3), child: ...)
// RotatedBox:布局阶段旋转,影响布局
RotatedBox(quarterTurns: 1, child: ...)
// 组合变换(顺序很重要!)
Transform(
transform: Matrix4.identity()
..translate(10.0)
..rotate(0.5)
..scale(1.5),
child: ...,
)
记忆技巧
- Transform = 绘制阶段 = 不影响布局 = 性能好
- RotatedBox = 布局阶段 = 影响布局 = 90度倍数
- 变换顺序 = 先平移后旋转 ≠ 先旋转后平移
- 弧度转换 = 角度 × π / 180